diff --git a/docs-site/src/content/docs/guides/sub-agent-surface.md b/docs-site/src/content/docs/guides/sub-agent-surface.md index ed8959161d2..40218f841f5 100644 --- a/docs-site/src/content/docs/guides/sub-agent-surface.md +++ b/docs-site/src/content/docs/guides/sub-agent-surface.md @@ -117,14 +117,14 @@ The dashboard's **Sub-agent delegation** controls three related settings: - `injectionModel` is the preferred worker model named in CodexCommander guidance. - `injectionEffort` is the optional `reasoning_effort` to request for that model. -- `injectionPrompt` replaces the built-in V2 guidance text. +- `injectionPrompt` customizes the V2 guidance body. When a preferred model or eligible roster resolves, explicit spawn-model instructions are retained. `multiAgentGuidanceEnabled` defaults to on and is the master switch for CodexCommander-authored guidance on both surfaces. Turning it off suppresses both the V2 designation block and V1 proactive text. These are instructions to the main agent, not a proxy-side spawn router. On V2, a full-history fork inherits the parent model and rejects model or effort overrides. Guidance therefore tells Codex to -use `fork_turns: "none"` (or a positive partial turn count such as `"3"`) when passing `model` or +use `fork_turns: "none"` when passing `model` or `reasoning_effort`, and to make the task message self-contained. ### Native continuation and caching @@ -151,7 +151,8 @@ Guidance is optional, sanitized operator text. Empty text is omitted; nonempty t Unicode code points. Treat it as untrusted advisory context, not as an effort, quota, role, or fallback control. On eligible V2 turns CodexCommander first applies live picker, surface, route, and encrypted-task compatibility filters, then adds every surviving row annotation to the built-in -developer message without an aggregate character budget. Row guidance is not injected on V1 and is +developer message or custom `{{roster}}` expansion without an aggregate character budget. +Row guidance is not injected on V1 and is never copied into the managed skill or global `AGENTS.md` policy. Custom `injectionPrompt` text can use all four placeholders: @@ -160,13 +161,19 @@ Custom `injectionPrompt` text can use all four placeholders: | --- | --- | | `{{model}}` | The effective preferred model for this request. A bare native `injectionModel` is account-qualified only when the request itself targets an explicit account selector. An unresolved or ambiguous bare value becomes an empty string; an unresolved explicit account-qualified or routed id remains unchanged | | `{{effort}}` | The configured `injectionEffort`, or an empty string | -| `{{roster}}` | The resolved picker-visible, surface-compatible roster | +| `{{roster}}` | The resolved picker-visible, surface-compatible roster and its eligible per-model guidance | | `{{fallback}}` | The configured global fallback guidance | Built-in V2 guidance includes every accepted roster annotation that survives the compatibility filters. Built-in guidance fires only when a preferred model, eligible roster, or fallback chain -resolves. A configured `injectionModel` is sufficient to render a custom prompt; if a bare value -cannot resolve uniquely, `{{model}}` expands to an empty string. +resolves. An eligible `injectionModel` is sufficient to render a custom prompt. If a bare value +cannot resolve uniquely, `{{model}}` expands to an empty string when another eligible roster or +fallback keeps the custom prompt active; otherwise guidance stays silent. + +When a preferred model or eligible roster resolves, built-in and custom guidance explain that a selected model must be passed explicitly in +`spawn_agent.model`; leaving it out inherits the native default or parent model. The managed skill +and `AGENTS.md` policy explicitly request task-fit overrides from the fresh, compatible live roster. +They contain no fixed model IDs, and native validation remains authoritative. On V1, CodexCommander injects only the upstream-style proactive delegation guidance at `max` or `ultra` effort. It does not add a preferred model, roster, fallback list, or custom prompt on V1. @@ -326,8 +333,9 @@ main agent still decides whether to delegate. ### Why did my V2 child use the parent model? -A full-history V2 fork inherits the parent model. Use a spawn that sets `fork_turns` to `"none"` or -a positive partial count before passing a model or effort override. +A full-history V2 fork inherits the parent model. Choose an eligible model from the live roster, +pass its exact ID in `spawn_agent.model`, and set `fork_turns` to `"none"` when using an override. +Refresh the managed delegation setup so its policy explicitly requests roster selection. ### Why is a configured model missing from the V2 roster? diff --git a/docs-site/src/content/docs/guides/web-dashboard.md b/docs-site/src/content/docs/guides/web-dashboard.md index b083cc7cc3c..074d269458e 100644 --- a/docs-site/src/content/docs/guides/web-dashboard.md +++ b/docs-site/src/content/docs/guides/web-dashboard.md @@ -214,7 +214,8 @@ than overwritten, so they may continue to override the requested defaults. Per-row roster guidance is separate live V2 developer text. It is optional sanitized operator text (empty omitted, at most 160 Unicode code points), advisory and untrusted rather than an effort, quota, -role, or fallback control. The built-in developer message includes every accepted annotation that +role, or fallback control. The built-in developer message and custom `{{roster}}` expansion include +every accepted annotation that survives the live V2 compatibility filters; it is not copied into the managed skill or `AGENTS.md`. :::caution @@ -224,10 +225,12 @@ they have been synchronized. See [Sub-agent Surface](/guides/sub-agent-surface/) for the canonical V1/base/V2 behavior. ::: -The spawn override guarantee applies to the **built-in** V2 guidance text. A custom -`injectionPrompt` replaces that text entirely and must include `{{model}}` and `{{effort}}` -placeholders (and optionally `{{roster}}`) or those values will not appear in the injected -guidance. +When a preferred model or eligible roster resolves, built-in and custom V2 guidance explain how to pass the selected model explicitly to +`spawn_agent.model`; omission inherits the native default or parent model. The managed delegation +skill and `AGENTS.md` policy explicitly request task-fit overrides when the live catalog is fresh +and the target is compatible. A custom `injectionPrompt` must still include `{{model}}`, `{{effort}}`, +or `{{roster}}` to include those configured values. The roster placeholder also includes eligible +per-model guidance. The picker offers enabled native and routed models plus the global Codex effort ladder. The API validates the selected effort globally; Codex still validates a spawn effort against the target diff --git a/docs-site/src/content/docs/reference/configuration/agents.md b/docs-site/src/content/docs/reference/configuration/agents.md index 836f9ea7717..0066c17cb72 100644 --- a/docs-site/src/content/docs/reference/configuration/agents.md +++ b/docs-site/src/content/docs/reference/configuration/agents.md @@ -15,7 +15,7 @@ routes, and limits delegated work. | `subagentModels?` | `{ model: string, guidance?: string }[] \| string[]` (on disk) | `gpt-5.5`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.4-mini` | Up to five ordered bare native, account-qualified `/`, or routed `provider/model` entries advertised first in the sub-agent picker. Reads accept either legacy strings or objects, but CodexCommander canonicalizes in memory and writes objects. The first object write is a migration point: older binaries that only understand `string[]` fail when they read the config. Optional `guidance` is sanitized operator text, capped at 160 Unicode code points; empty guidance is omitted. The dashboard preserves configured exact selectors and reports which saved entries are advertised or excluded. Use `ccx agent subagents set` or edit the configuration for choices that are not in the current catalog. An explicit empty list is preserved. | | `injectionModel?` | `string` | — | Preferred native or routed sub-agent model used in proxy-authored V2 delegation guidance. | | `injectionEffort?` | `string` | — | Preferred effort (`low` through `ultra`), meaningful only with `injectionModel`. | -| `injectionPrompt?` | `string` | — | Replaces the built-in V2 guidance body. Supports `{{model}}`, `{{effort}}`, `{{roster}}`, and `{{fallback}}`. A configured `injectionModel` is sufficient to render the custom prompt. | +| `injectionPrompt?` | `string` | — | Customizes the V2 guidance body. When a preferred model or eligible roster resolves, the explicit spawn-model instructions are retained. Supports `{{model}}`, `{{effort}}`, `{{roster}}`, and `{{fallback}}`; the roster includes eligible per-model guidance. An eligible `injectionModel` is sufficient to render the custom prompt. | | `multiAgentGuidanceEnabled` | `boolean` | `true` | Controls only CodexCommander-authored V1/V2 developer guidance; it does not change native agent defaults, tools, routing, rosters, or effort caps. | | `syncCodexSubagentDefaults?` | `boolean` | `false` | Opt into writing `injectionModel` and optional `injectionEffort` as Codex's native defaults during sync/restart. Requires `injectionModel`. | | `subagentModelFallback?` | `string[]` | `[]` | Priority-ordered global fallback models for spawned child turns. | @@ -58,21 +58,27 @@ Surface detection uses tool shape. A namespaced `spawn_agent` with `send_input`, V1 guidance is proactive text only at `max` or `ultra`. V2 receives a proxy-authored developer message only when a preferred model, eligible roster, or fallback chain exists. Built-in V2 guidance -includes every accepted eligible roster annotation; it has no aggregate character budget. Guidance +and the custom `{{roster}}` placeholder include every accepted eligible roster annotation; there is no +aggregate character budget. Guidance is deduplicated across replay prefixes and inserted before a trailing `compaction_trigger`. `injectionModel` and `injectionEffort` are advisory unless native-default sync is enabled. The built-in V2 text treats the featured roster as suggestions, allows a known exact compatible catalog ID to be passed to native `spawn_agent`, and asks Codex to pass supported model/effort overrides with -`fork_turns: "none"`. A custom `injectionPrompt` substitutes missing values with an empty string and -replaces that built-in exact-ID wording. +`fork_turns: "none"`. A custom `injectionPrompt` substitutes missing values with an empty string. +When a preferred model or eligible roster exists, both paths explain that the selected exact ID must +be passed in `spawn_agent.model`: omission inherits the native default or parent rather than selecting +from the roster. The managed delegation skill and `AGENTS.md` policy explicitly request these task-fit +overrides, satisfying native Codex's requirement for a user, policy, or skill instruction. Catalog +freshness, compatibility, and native validation still apply. Each roster row may also carry optional `guidance`. This is untrusted operator text for the live delegation message, not a policy control: it cannot set effort, quotas, roles, or fallback behavior. After normalization (NFC plus trim), empty text is omitted; nonempty text is limited to 160 Unicode code points and sanitized before persistence. Row guidance is considered only on eligible V2 turns, after live picker, route, surface, and encrypted-task compatibility filters. Every accepted annotation -that survives those filters is included in built-in V2 guidance. It is not injected on V1 and is +that survives those filters is included in built-in V2 guidance and custom `{{roster}}` expansion. +It is not injected on V1 and is never copied into the managed skill or global `AGENTS.md` block. ## Managed advisory setup diff --git a/src/codex/delegation-templates.ts b/src/codex/delegation-templates.ts index fdb7973700d..11bef27dc63 100644 --- a/src/codex/delegation-templates.ts +++ b/src/codex/delegation-templates.ts @@ -41,6 +41,7 @@ function agentsBlock(mode: CodexDelegationMode): string { `Mode: ${mode}`, "Before spawning subagents, use $codexcommander-delegation and reread its SKILL.md if its details were compacted.", "Consult the active collaboration roster and spawn-tool contract. Never hardcode model IDs. Match each spawn to task complexity, uncertainty, risk, and how tightly you can check the result; reassess on failed evidence, scope expansion, uncertainty, or capability mismatch—not on timeout alone. Give workers self-contained tasks.", + "This policy explicitly requests model overrides: when a fresh Commander roster or preferred model provides a task-fit choice compatible with the active tool and task delivery, pass its exact ID in spawn_agent.model. Omitting model inherits the configured default or parent; it does not select from the roster. Respect stale or unknown catalogs and unsupported overrides; on V2 use fork_turns none when setting an override.", "A wait_agent timeout is neutral; never interrupt solely because one or more waits timed out. Use the managed skill for reconciliation and interruption gates.", delegationSentence, "This guidance is advisory and does not create collaboration tools. User and repository instructions outrank it for whether to spawn.", diff --git a/src/server/responses/collaboration.ts b/src/server/responses/collaboration.ts index 23a5a818863..994d6fc2b52 100644 --- a/src/server/responses/collaboration.ts +++ b/src/server/responses/collaboration.ts @@ -424,17 +424,23 @@ export async function multiAgentGuidanceText( const fallbackGuidance = subagentFallbackGuidanceText({ subagentModelFallback: compatibleFallback } as CodexCommanderConfig); const hasCompatibleGuidance = preferred !== undefined || compactRoster !== "" || fallbackGuidance !== ""; if (!hasCompatibleGuidance) return null; + const modelSelectionContract = preferred !== undefined || compactRoster !== "" + ? "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, " + + "pass the chosen exact model in spawn_agent.model. " + + "Omitting model inherits the configured default or parent; it does not select from the Commander roster. " + + "When setting either override, use fork_turns \"none\" and a self-contained task." + : ""; if (injectionPrompt) { - return `${applyInjectionPlaceholders(injectionPrompt, preferred?.model, injectionEffort, compactRoster, fallbackGuidance)}`; + const custom = applyInjectionPlaceholders(injectionPrompt, preferred?.model, injectionEffort, annotatedRoster, fallbackGuidance); + return `${custom}${modelSelectionContract ? ` ${modelSelectionContract}` : ""}`; + } + let text = modelSelectionContract; + if (modelSelectionContract) { + text += " Listed models are featured suggestions, not an exhaustive allowlist. " + + "A known exact catalog ID that is fresh, routable, and compatible with this surface and task delivery " + + "may be passed directly to native spawn_agent; native validation remains authoritative. " + + "Do not invent IDs or substitute another family."; } - let text = "When the active spawn_agent tool supports optional \"model\" or \"reasoning_effort\" overrides, " - + "listed models are featured suggestions, not an exhaustive allowlist. " - + "A known exact catalog ID that is fresh, routable, and compatible with this surface and task delivery " - + "may be passed directly to native spawn_agent; native validation remains authoritative. " - + "Do not invent IDs or substitute another family. " - + "When setting either override, set fork_turns to \"none\" " - + "(or a positive turn count such as \"3\"; full-history forks reject overrides) " - + "and make the task message self-contained."; if (preferred) { text += ` Preferred sub-agent: model "${preferred.model}"` + (injectionEffort ? `, reasoning_effort "${injectionEffort}"` : "") @@ -442,7 +448,7 @@ export async function multiAgentGuidanceText( } text += fallbackGuidance; text += annotatedRoster; - return `${text}`; + return `${text.trim()}`; } const effort = parsed.options.reasoning; diff --git a/src/skills/codexcommander-delegation/SKILL.md b/src/skills/codexcommander-delegation/SKILL.md index 09692eb0c3a..16d88648fa6 100644 --- a/src/skills/codexcommander-delegation/SKILL.md +++ b/src/skills/codexcommander-delegation/SKILL.md @@ -28,7 +28,7 @@ Give every child a self-contained brief with its goal, paths or inputs, owned fi Match each spawn to task fit: complexity, uncertainty, risk, and how tightly you can check the result. Live featured models are suggestions, not an exhaustive allowlist. Prefer the configured preferred worker when it fits. Escalate capability only when the brief needs it; do not default to the cheapest or smallest model, and do not spawn solely to delegate. -Omitting `model` inherits the configured default or parent choice. When the live contract permits a deliberate override and you know an exact compatible catalog ID that is fresh, routable, and compatible with this surface and task delivery, pass that known exact ID as `model`. Add `reasoning_effort` only when the target advertises supported values and the brief benefits from it. On V2, set `fork_turns` to `"none"` or a positive partial count when passing `model` or `reasoning_effort`; full-history forks reject overrides. Omit overrides when the catalog is stale, unknown, or uncertain. Native validation remains authoritative. Do not invent IDs, substitute another family, or remember IDs. +This skill explicitly requests model overrides when a fresh Commander roster or preferred model provides a task-fit choice compatible with the active tool and task delivery. Choose using the live model guidance and pass the selected known exact catalog ID as `model` in `spawn_agent.model`; listing a roster alone does not select a worker. Omitting `model` inherits the configured default or parent choice, so do not omit it merely because the native tool inherits by default. Add `reasoning_effort` only when the target advertises supported values and the brief benefits from it. On V2, set `fork_turns` to `"none"` when passing `model` or `reasoning_effort`; full-history forks reject overrides. Omit overrides when the catalog is stale, unknown, or uncertain, the tool does not support them, or no compatible choice fits. Native validation remains authoritative. Do not invent IDs, substitute another family, or remember IDs. ## Root review and reassessment diff --git a/structure/03_catalog-and-subagents.md b/structure/03_catalog-and-subagents.md index 9611263af79..9adf6d3df52 100644 --- a/structure/03_catalog-and-subagents.md +++ b/structure/03_catalog-and-subagents.md @@ -326,7 +326,7 @@ the requested model id only; effort remains owned by the caps described under On eligible V2 turns, a surviving row's guidance is included in the live developer message only after the current catalog's surface, visibility, route, and encrypted-task compatibility filters have run. Every accepted annotation that survives those filters is included in the built-in V2 -message, which has no aggregate character budget. It is never copied into the managed delegation +message and custom `{{roster}}` expansion, with no aggregate character budget. It is never copied into the managed delegation skill or global `AGENTS.md` block, and it does not change native Codex behavior on V1. `injectionModel` and `injectionEffort` are shared selections with two independent consumers. @@ -342,6 +342,13 @@ Native `[agents]` defaults remain limited to `injectionModel` and `injectionEffo per-row guidance are not persisted there. The managed skill and bounded global `AGENTS.md` policy are also deliberately roster-free and consult the live collaboration contract instead. +The managed skill and global policy explicitly request an exact model override for a fresh, +compatible task-fit roster choice. This supplies the instruction source required by native Codex's +model-override contract. Both built-in and custom V2 model guidance explain that omission inherits +the native default or parent; merely advertising a roster does not select a worker. Custom prompts +retain that instruction when an eligible model is designated. Native validation and the stale/unknown +catalog gates remain authoritative, and full-history forks still reject overrides. + Claude Code `ccx-*` agent definitions consume the same effective `claudeCode.blockedSkills` policy as inbound bundle elision. When the list is non-empty (default: `claude-api`), generated definitions whose marker-stripped model resolves to a routed id receive a preventive instruction not to invoke diff --git a/tests/codex-delegation-templates.test.ts b/tests/codex-delegation-templates.test.ts index d4aa2da52ad..f7884399f1d 100644 --- a/tests/codex-delegation-templates.test.ts +++ b/tests/codex-delegation-templates.test.ts @@ -64,6 +64,18 @@ describe("Codex delegation templates", () => { expect(bundle.agentsBlockText).toContain("synthesis"); }); + test.each(["balanced", "orchestrator"] as const)("%s explicitly requests roster model overrides in both managed instruction sources", mode => { + const bundle = renderCodexDelegationBundle(mode); + for (const instructions of [bundle.skillText, bundle.agentsBlockText]) { + expect(instructions).toContain("explicitly requests model overrides"); + expect(instructions).toContain("spawn_agent.model"); + expect(instructions).toContain("inherits"); + expect(instructions).toContain("fresh"); + expect(instructions).toContain("compatible"); + } + expect(bundle.skillText).not.toContain("positive partial count"); + }); + test.each(["balanced", "orchestrator"] as const)( "%s manual setup follows platform default discovery instead of equating an unset CODEX_HOME with the Unix default", (mode) => { diff --git a/tests/multi-agent-compat.test.ts b/tests/multi-agent-compat.test.ts index c961a3c3856..b47fa9342b7 100644 --- a/tests/multi-agent-compat.test.ts +++ b/tests/multi-agent-compat.test.ts @@ -275,7 +275,8 @@ describe("multiAgentGuidanceText", () => { injectionPrompt: "Use {{model}}.", }, ); - expect(custom).toBe('Use team/gpt-5.6-sol.'); + expect(custom).toContain("Use team/gpt-5.6-sol."); + expect(custom).toContain("spawn_agent.model"); const exactBare = await multiAgentGuidanceText( parsedFixture({ tools: [{ name: "spawn_agent" }] }), @@ -294,7 +295,8 @@ describe("multiAgentGuidanceText", () => { injectionPrompt: "Use {{model}}.", }, ); - expect(exactBareCustom).toBe("Use local-fast."); + expect(exactBareCustom).toContain("Use local-fast."); + expect(exactBareCustom).toContain("spawn_agent.model"); const bareParent = await multiAgentGuidanceText( parsedFixture({ tools: [{ name: "spawn_agent" }] }), @@ -602,6 +604,32 @@ describe("multiAgentGuidanceText", () => { expect(v2).not.toContain("Must not appear"); }); + test("custom V2 guidance keeps the spawn model contract and eligible roster annotations", async () => { + const dir = codexHomeFixture(V2_ON); + catalogFixture(dir, [ + { slug: "vendor/scoped-worker", efforts: ["high"] }, + { slug: "native-reviewer", efforts: ["high", "max"] }, + ]); + const text = await multiAgentGuidanceText( + parsedFixture({ tools: [{ name: "spawn_agent" }] }), + { + injectionPrompt: "CUSTOM task-fit instructions. {{roster}}", + subagentRoster: [ + { model: "vendor/scoped-worker", guidance: "Use for bounded implementation" }, + { model: "native-reviewer", guidance: "Use for difficult review" }, + { model: "missing/worker", guidance: "Excluded guidance must remain absent" }, + ], + }, + ); + expect(text).toContain("CUSTOM task-fit instructions."); + expect(text).toContain("spawn_agent.model"); + expect(text).toContain("Omitting model inherits"); + expect(text).toContain("Use for bounded implementation"); + expect(text).toContain("Use for difficult review"); + expect(text).not.toContain("Excluded guidance must remain absent"); + expect(text).not.toContain("positive turn count"); + }); + test("bare-native notes do not attach to unrelated provider rows", async () => { const dir = codexHomeFixture(V2_ON); catalogFixture(dir, [ @@ -706,7 +734,7 @@ describe("multiAgentGuidanceText", () => { expect(text).not.toContain("Incompatible routed note"); }); - test("custom roster placeholder omits roster notes", async () => { + test("custom roster placeholder includes eligible roster notes", async () => { const dir = codexHomeFixture(V2_ON); catalogFixture(dir, [{ slug: "xai/grok-4.6", @@ -726,7 +754,7 @@ describe("multiAgentGuidanceText", () => { expect(text).toContain('"xai/grok-4.6"'); expect(text).toContain("low/medium/high/xhigh"); - expect(text).not.toContain("Use for independent review"); + expect(text).toContain("Use for independent review"); }); test("native encrypted V2 guidance advertises only routes with encrypted-task capability", async () => { @@ -864,10 +892,8 @@ describe("multiAgentGuidanceText", () => { }, ); - expect(text).toBe( - 'CUSTOM model= effort=max' - + ' Available models (reasoning_effort high/max): "gpt-5.6-terra".', - ); + expect(text).toContain('CUSTOM model= effort=max Available models (reasoning_effort high/max): "gpt-5.6-terra".'); + expect(text).toContain("spawn_agent.model"); expect(text).not.toContain("gpt-5.6-luna"); });