You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#12323 adds a rule to the bundled-reference route resolver: under ToolMode.CodeModeOnly, registration is not reachability, because CodeModeOnly hides both bridge tools (packages/core/src/tools/code-mode.ts:28 — HIDDEN_TOOLS = new Set([ToolNames.TOOL_SEARCH, 'tool_call']), surfaced as 'hidden' by getToolExposure), so a bridge pointer would name tools the session cannot call. The guard sits at packages/core/src/skills/bundled-reference.ts:152:
The sibling export eighty lines below, isToolHiddenBehindToolSearch (:234-240), keeps the pure registration + visibility check with no tool-mode arm — and the consumer that turns it into a bridge instruction can still emit one. packages/cli/src/ui/utils/workflow-keyword.ts:146 calls it for ToolNames.WORKFLOW, reached from AppContainer.tsx:3187 on every user message containing "workflow", gated only on ui.disableWorkflowKeywordTrigger and shell mode. When it returns true and both bridge tools are in getAllToolNames(), the hook sets bridgeWorkflowTool = true and the steering notice carries toolSearchBridgeSentence(ToolDisplayNames.WORKFLOW) — telling the model to review the schema with tool_search and invoke it with tool_call, instead of pointing at tools.workflow(...) inside exec, the only real route there.
With tools.codeModeOnly: true plus a tools.eager allowlist omitting workflow, every part of that path holds:
registerLazyTool marks Workflow permission-deferred regardless of mode (packages/core/src/config/config.ts:11089, registerPermissionDeferredFactory);
both bridge tools are registered unconditionally in the standard branch (config.ts:11370-11377, under the // --- Core tools (always registered) --- comment);
so isToolDeferredBehindToolSearch returns true, the consumer's "both halves registered" test passes, and each triggered turn injects a reminder towards tools that getToolExposure marks hidden, getCodeModeFunctionDeclarations never declares and isCodeModeToolCallAllowed refuses.
The predicate's body is byte-identical to the merge-base version — the blind spot is carried over rather than introduced — but the guard landing beside it in the same file is what makes the contradiction shipped and adjacent. The route path's own new comment names exactly this outcome.
Prevalence is unmeasured: the review that filed this used a stub Config, not an end-to-end session, and found no shipping preset combining codeModeOnly with an eager allowlist that omits workflow. What ships is a documented contradiction between two neighbours in one file, not an observed field failure.
What I expected
One rule, applied to both exports. A tool the session cannot call should not be named in a bridge instruction, whichever function is asked.
Suggested fix
Give the shared predicate the same mode check, which fixes both arms at once — at the top of isToolDeferredBehindToolSearch:
or guard at the workflow-keyword.ts:146 call site if the predicate's other callers should keep the registration-only answer.
Two constraints:
config.ts:11370-11377 registers TOOL_CALL and TOOL_SEARCH unconditionally, so a fix cannot infer CodeModeOnly from the bridge tools being absent from getAllToolNames().
It must not change the CodeModeOnly route away from 'skill'. That is safe by construction at perf(core): move the Agent tool's prompt-writing guidance into a bundled skill #12323's head — the route resolver short-circuits before calling the predicate — but it is pinned by workflow-authoring-skill.test.ts's points straight at the skill when CodeModeOnly hides the bridge, so a fix that moved the guard into the resolver instead of the predicate would show up there.
Acceptance test
Must go red when the mode check is removed:
a workflow-keyword.test.ts case with getToolMode() → ToolMode.CodeModeOnly and workflow permission-deferred, asserting the returned prefix does not contain toolSearchBridgeSentence(ToolDisplayNames.WORKFLOW); and/or
an it.each row ['deferred under CodeModeOnly', { deferred: [ToolNames.WORKFLOW], toolMode: ToolMode.CodeModeOnly }, false] — the existing stub already accepts toolMode.
Filed from R2-3 in #12323's round-2 review, which was explicitly left out of that PR as its own change: #12323 (comment)
What happened
#12323 adds a rule to the bundled-reference route resolver: under
ToolMode.CodeModeOnly, registration is not reachability, because CodeModeOnly hides both bridge tools (packages/core/src/tools/code-mode.ts:28—HIDDEN_TOOLS = new Set([ToolNames.TOOL_SEARCH, 'tool_call']), surfaced as'hidden'bygetToolExposure), so a bridge pointer would name tools the session cannot call. The guard sits atpackages/core/src/skills/bundled-reference.ts:152:The sibling export eighty lines below,
isToolHiddenBehindToolSearch(:234-240), keeps the pure registration + visibility check with no tool-mode arm — and the consumer that turns it into a bridge instruction can still emit one.packages/cli/src/ui/utils/workflow-keyword.ts:146calls it forToolNames.WORKFLOW, reached fromAppContainer.tsx:3187on every user message containing "workflow", gated only onui.disableWorkflowKeywordTriggerand shell mode. When it returns true and both bridge tools are ingetAllToolNames(), the hook setsbridgeWorkflowTool = trueand the steering notice carriestoolSearchBridgeSentence(ToolDisplayNames.WORKFLOW)— telling the model to review the schema withtool_searchand invoke it withtool_call, instead of pointing attools.workflow(...)insideexec, the only real route there.With
tools.codeModeOnly: trueplus atools.eagerallowlist omittingworkflow, every part of that path holds:registerLazyToolmarks Workflow permission-deferred regardless of mode (packages/core/src/config/config.ts:11089,registerPermissionDeferredFactory);config.ts:11370-11377, under the// --- Core tools (always registered) ---comment);isToolDeferredBehindToolSearchreturns true, the consumer's "both halves registered" test passes, and each triggered turn injects a reminder towards tools thatgetToolExposuremarks hidden,getCodeModeFunctionDeclarationsnever declares andisCodeModeToolCallAllowedrefuses.The predicate's body is byte-identical to the merge-base version — the blind spot is carried over rather than introduced — but the guard landing beside it in the same file is what makes the contradiction shipped and adjacent. The route path's own new comment names exactly this outcome.
Prevalence is unmeasured: the review that filed this used a stub
Config, not an end-to-end session, and found no shipping preset combiningcodeModeOnlywith an eager allowlist that omitsworkflow. What ships is a documented contradiction between two neighbours in one file, not an observed field failure.What I expected
One rule, applied to both exports. A tool the session cannot call should not be named in a bridge instruction, whichever function is asked.
Suggested fix
Give the shared predicate the same mode check, which fixes both arms at once — at the top of
isToolDeferredBehindToolSearch:or guard at the
workflow-keyword.ts:146call site if the predicate's other callers should keep the registration-only answer.Two constraints:
config.ts:11370-11377registersTOOL_CALLandTOOL_SEARCHunconditionally, so a fix cannot infer CodeModeOnly from the bridge tools being absent fromgetAllToolNames().'skill'. That is safe by construction at perf(core): move the Agent tool's prompt-writing guidance into a bundled skill #12323's head — the route resolver short-circuits before calling the predicate — but it is pinned byworkflow-authoring-skill.test.ts'spoints straight at the skill when CodeModeOnly hides the bridge, so a fix that moved the guard into the resolver instead of the predicate would show up there.Acceptance test
Must go red when the mode check is removed:
workflow-keyword.test.tscase withgetToolMode() → ToolMode.CodeModeOnlyandworkflowpermission-deferred, asserting the returned prefix does not containtoolSearchBridgeSentence(ToolDisplayNames.WORKFLOW); and/orit.eachrow['deferred under CodeModeOnly', { deferred: [ToolNames.WORKFLOW], toolMode: ToolMode.CodeModeOnly }, false]— the existing stub already acceptstoolMode.Filed from R2-3 in #12323's round-2 review, which was explicitly left out of that PR as its own change: #12323 (comment)