diff --git a/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md b/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md index 0c8c825344..bf87a1a790 100644 --- a/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md +++ b/devlog/_plan/260817_wave5_execution/090_wave6_closeout.md @@ -41,3 +41,38 @@ resolved: close-on-dev vs close-on-release; #1059 consecutive-green count; Cursor non-loopback HTTP; Antigravity undocumented protocol posture; needs-info lifetime; upstream-tracker accounting; #1795 recovery shape; #1899 disposition; #1836 disposition; #1903 HTTP/1.1 default. +## Closure policy for THIS run (user decision, 2026-08-18) + +> "이슈는 dev 머지되면 일단 닫아놔 이번 런만 그런거야" + +Close issues when the fix reaches `dev`, not when it reaches a stable release. **Scoped to this +run only** — the standing preference remains close-on-release, so a future campaign should not +read this as precedent. + +What this changes: the `released-in:vX.Y.Z` step no longer gates closure. What it does *not* +change is the evidence bar — a close still needs the fix demonstrably on `origin/dev` by +ancestry, and still must not close an umbrella from a partial fix. The three policy holds keep +their own reasons, which are about missing evidence rather than about release timing: + +| Issue | Still open because | +|-------|--------------------| +| #1059 | needs hosted Windows shard evidence; no local batch substitutes | +| #1795 | needs a live SenseNova/Kimi canary showing zero undeclared calls | +| #1852 | the reported defect (sync enumeration blocking the event loop) is #1876's unmerged async work | +| #1849 | umbrella; its root cause is #1942 and unstarted | +| #1049 | assessed and unstarted; needs the publication protocol | +| #1926 | destination scope landed, but credential scope and emit-before-commit are still live in `src/bridge.ts` | +| #1866 | explicitly scoped out of #1900; no PR addresses it | +| #1730 | different provider and round from #1884's ClinePass replay fix | + +Two **pull requests** are also held, and they belong in this record even though the table +above is about issues — a reader working only from this document would otherwise see no trace +of them: + +| PR | Held because | +|----|--------------| +| #1891 | it makes `GOOGLE_ANTIGRAVITY_USER_AGENT` steerable into the `onboardUser` request body, violating this wave's accept criterion. Needs #1889 first, which is the one-line fix that makes `ide_version` a real constant. Detail in `080`. | +| #1889 | unsponsored `src/oauth/` surface, plus still draft. The `maintainer-sponsored` label is the record that a security review happened, so an agent applying it would falsify that record. | + +Neither is affected by the close-on-dev-merge decision: both are blocked *before* merge, so the +policy that governs when a merged fix closes its issue never reaches them. diff --git a/src/adapters/tool-catalog-nudge.ts b/src/adapters/tool-catalog-nudge.ts index 913ce12584..9325125691 100644 --- a/src/adapters/tool-catalog-nudge.ts +++ b/src/adapters/tool-catalog-nudge.ts @@ -28,12 +28,25 @@ const NEIGHBOR_AGENT_TOOL_NAMES = ["Read", "Grep", "Glob", "Bash", "LS"] as cons const CODEX_UNIFIED_EXEC_TOOL_NAME = "exec"; const CODEX_SHELL_BRIDGE_TOOL_NAMES = ["exec_command", "shell_command"] as const; -function isCodexCodeModeExecTool(tool: Pick): boolean { - return tool.name === CODEX_UNIFIED_EXEC_TOOL_NAME && tool.freeform === true; +function isCodexCodeModeExecTool(tool: Pick): boolean { + return !tool.namespace && tool.name === CODEX_UNIFIED_EXEC_TOOL_NAME && tool.freeform === true; } -function isBareShellBridgeTool(tool: Pick): boolean { - return (CODEX_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(tool.name); +/** + * BARE means un-namespaced, and the word is load-bearing. + * + * An MCP server can advertise its own `exec_command` or `shell_command` — a docker, k8s or ssh + * server plausibly does — and those arrive namespaced (`mcp__docker__exec_command`). They are + * not Codex's shell bridge, so they must not cancel code mode: a genuine code-mode turn that + * merely happens to sit beside an MCP shell tool would lose its guidance and fall back to the + * generic sentence. + * + * The Cursor original this was ported from (`isBareCodexShellBridgeTool`) carries the same + * `!tool.namespace` requirement; dropping it here made the name assert a check the body did not + * perform. + */ +function isBareShellBridgeTool(tool: Pick): boolean { + return !tool.namespace && (CODEX_SHELL_BRIDGE_TOOL_NAMES as readonly string[]).includes(tool.name); } function quoteNames(names: readonly string[]): string { diff --git a/tests/tool-catalog-nudge.test.ts b/tests/tool-catalog-nudge.test.ts index 91d4ec7b54..f764f001fa 100644 --- a/tests/tool-catalog-nudge.test.ts +++ b/tests/tool-catalog-nudge.test.ts @@ -133,6 +133,30 @@ describe("non-OpenAI tool catalog nudge", () => { expect(note).toContain("`custom_exec` is Codex code mode"); }); + // "Bare" means un-namespaced. An MCP server can advertise its own `exec_command` — docker, + // k8s and ssh servers plausibly do — and that is not Codex's shell bridge. Letting it cancel + // code mode silently strips the guidance from a genuine code-mode turn, which is how the + // Cursor original (`isBareCodexShellBridgeTool`) has always read it. + test("a namespaced MCP shell tool does not cancel code mode", () => { + for (const name of ["exec_command", "shell_command"]) { + const note = buildNonOpenAIToolCatalogNudgeForTools([ + codeModeExec(), + { namespace: "mcp__docker", name, parameters: {} } as OcxTool, + ]); + + expect(note).toContain("is Codex code mode"); + } + }); + + test("a namespaced freeform exec is not Codex's own code-mode tool", () => { + const note = buildNonOpenAIToolCatalogNudgeForTools([ + { namespace: "mcp__sandbox", name: "exec", freeform: true, parameters: {} } as OcxTool, + ]); + + expect(note).not.toContain("is Codex code mode"); + expect(note).toContain("call the listed parent tool and use those helpers only inside that tool's input"); + }); + // `advertised` holds WIRE names. A provider that rewrites them (Claude OAuth `custom_`, // Anthropic compat `cx_`) must not have every neighbor name declared unavailable while the // catalog plainly lists the prefixed form.