diff --git a/.changeset/2009-load-failed-capability-fail-open.md b/.changeset/2009-load-failed-capability-fail-open.md new file mode 100644 index 000000000..22ce740cf --- /dev/null +++ b/.changeset/2009-load-failed-capability-fail-open.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2075 +--- +**Load-failed capability gates now fail open with a loud warning instead of blocking the whole project** — when an installed overlay (third-party) capability failed to load (e.g. an incompatible `engines.gsd` range) but had declared a `gate`-kind loop hook, the loop resolver injected a blocking synthetic gate (`blocking:true`, `onError:halt`) at every point where that capability declared a gate. A single incompatible capability therefore halted every `ship:pre` and `verify:post` in the project — unrelated to what the gate would have checked, and with no remediation surfaced. The resolver now injects no gate and instead emits a loud warning — to stderr and in the `loop render-hooks` envelope's `warnings` array — naming the load-failure reason and the exact `gsd capability remove ` remediation, and the loop proceeds (fail open). The capability id embedded in that remediation is validated against the canonical id shape first, so a malformed overlay directory name cannot inject shell metacharacters into the surfaced command. The loader still records `_overlay.blockedGates`; only the consequence changes from block to warn. `step`/`contribution` overlays were already skip-open. (#2009) diff --git a/.changeset/2072-thread-model-into-routed-agent-spawns.md b/.changeset/2072-thread-model-into-routed-agent-spawns.md new file mode 100644 index 000000000..a74b21ff9 --- /dev/null +++ b/.changeset/2072-thread-model-into-routed-agent-spawns.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2074 +--- +**`model_overrides` and per-phase-type models now actually apply to the assumptions-analyzer, code-reviewer, and code-fixer agents on Claude Code.** Previously `model_overrides["gsd-code-reviewer"]` / `["gsd-assumptions-analyzer"]` / `["gsd-code-fixer"]` (and `models.verification` / `models.discuss` / `models.execution`) were accepted and resolved but silently dropped — the workflows spawned these agents with no model, so they inherited the session model and the configured routing never took effect (no warning). Every spawn now threads its resolved model: `discuss-phase-assumptions`, `code-review`, and `code-review-fix` (both the re-review and the two fixer spawns) resolve it inline, and `quick`'s review step uses the code-reviewer's own resolved model instead of the executor's. The stale "`discuss` — reserved, no subagent" model-profile docs are corrected to list `gsd-assumptions-analyzer`, and the `verification` row now includes `gsd-code-reviewer`. (#2074) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 760409bcd..59ef61380 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -381,7 +381,7 @@ Node.js CLI utility (`gsd-tools.cjs`) with domain modules split across `gsd-core | `profile-pipeline.cjs` | User behavioral profiling data pipeline, session file scanning | | `profile-output.cjs` | Profile rendering, USER-PROFILE.md and dev-preferences.md generation | | `loop-host-contract.cjs` | Generated Loop Host Contract — 12 loop points, per-step agent roles, and core artifacts; emitted by `scripts/gen-loop-host-contract.cjs` from workflow markers (ADR-894 §3); consumed by `gen-capability-registry.cjs` | -| `capability-loader.cjs` | Runtime registry overlay loader (ADR-1244 D2) — `loadRegistry({ includeInstalled })` composes the frozen first-party registry with a validated installed overlay of third-party capability manifests read from global `$GSD_HOME/.gsd/capabilities/` and project `/.gsd/capabilities/`; first-party always wins; load-time `engines.gsd` re-gate skips incompatible overlays with a warning; gate-kind hooks on skipped capabilities fail CLOSED | +| `capability-loader.cjs` | Runtime registry overlay loader (ADR-1244 D2) — `loadRegistry({ includeInstalled })` composes the frozen first-party registry with a validated installed overlay of third-party capability manifests read from global `$GSD_HOME/.gsd/capabilities/` and project `/.gsd/capabilities/`; first-party always wins; load-time `engines.gsd` re-gate skips incompatible overlays with a warning; gate-kind hooks on skipped capabilities fail OPEN — no gate is injected; a loud warning (stderr + envelope `warnings`) names the load failure and the `gsd capability remove ` remediation (#2009) | | `capability-registry.cjs` | Generated central Capability Registry — role-partitioned index of all co-located capability declarations; emitted by `scripts/gen-capability-registry.cjs` (ADR-894 §5) | | `loop-resolver.cjs` | Loop Extension Point resolver — ADR-857 phase 3c registry-consuming query; consumes resolved Capability State, filters `byLoopPoint` by capability enablement plus config activation, renders active hooks as markdown, emits `{ point, activeHooks, rendered }` envelope; `gsd-tools loop render-hooks [--config-dir ]` | | `capability-state.cjs` | Unified capability-state resolver — ADR-857 phase 4b/6; composes install profile, runtime surface, and config activation into one per-capability view consumed by workflow hook rendering; pure `resolveCapabilityState`, reusable `resolveCapabilityRuntimeState`, I/O `cmdCapabilityState`, and convenience predicate `isCapabilityActive(capId, cwd)`; `gsd-tools capability state [--config-dir ]` emits `{ runtimeConfigDir, capabilities[] }` where each entry carries `enabled` (installed && surfaced) and `active` (enabled && configActivation via the capability's `activationKey`; absent key → active===enabled) | diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index d754a4887..7505907e9 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -770,9 +770,9 @@ Installed overlay capabilities are merged via the same `buildRegistry` pipeline Each overlay manifest may declare an `engines.gsd` semver range. At load time GSD evaluates this range against the running GSD version. An overlay that does not satisfy the range is **skipped with a warning** — it is never loaded and never crashes the loop. Manifests without an `engines.gsd` field are accepted unconditionally. -### Gate-kind fail-closed policy +### Gate-kind fail-open policy (#2009) -If a skipped overlay capability declared a `gate`-kind loop hook, the loop resolver **injects a blocking gate** at that hook point (fail CLOSED). Skipped capabilities whose hooks are `step` or `contribution` kind skip open — the loop proceeds without them. +If a skipped or load-failed overlay capability (for example, one whose `engines.gsd` range is incompatible) declared a `gate`-kind loop hook, the loop resolver does **not** inject a gate at that hook point (fail OPEN): the loop proceeds. Instead it emits a loud warning — to stderr and in the `loop render-hooks` envelope's `warnings` array — naming the load-failure reason and the exact remediation, `gsd capability remove `, so the operator is loudly told how to clear it. Skipped capabilities whose hooks are `step` or `contribution` kind skip open too, as before — the loop proceeds without them. ### Overlay config federation @@ -1115,10 +1115,10 @@ for the change to take effect. See issue #2256. | Phase type | Agents | |---|---| | `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` | -| `discuss` | (reserved — no subagent today) | +| `discuss` | `gsd-assumptions-analyzer` | | `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` | | `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` | -| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` | +| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` | | `completion` | (reserved — no subagent today) | `discuss` and `completion` are accepted by the schema for forward compatibility; setting them today is a no-op until a subagent maps to them. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 1a80bd7c3..2a3bcf584 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -2751,10 +2751,10 @@ Users who run a memory / knowledge-base MCP server (for example, ExoCortex-style | Slot | Agents assigned | |------|-----------------| | `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` | -| `discuss` | (reserved for future subagent) | +| `discuss` | `gsd-assumptions-analyzer` | | `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` | | `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` | -| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` | +| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` | | `completion` | (reserved for future subagent) | **Accepted values:** `"opus"` / `"sonnet"` / `"haiku"` / `"inherit"` diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index be47f0f48..af1075362 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -407,7 +407,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`. | `capability-lock.cjs` | Shared cross-process lock primitive (#1459 finding 4) — the SINGLE hardened lockfile protocol used by BOTH capability-lifecycle (`.gsd/capabilities/.lock`) and capability-consent (`.consent.lock`); exports `acquireLock(lockPath, opts?)`/`releaseLock(handle)` with pid + process-start-time liveness identity, a hard deadman, and token+inode owner-safe release — NEVER stale-steals a verified-live same-host holder, reclaims only a provably-dead/unverifiable holder, never deadlocks; `opts.maxAttempts`/`opts.waitForFresh` let the consent store serialize genuinely-contended writers; `_setLockProbes`/`_resetLockProbes` are test seams | | `capability-ledger.cjs` | Per-runtime install ledger (ADR-1244 D4) — atomic read/write of `.gsd-capabilities.json` recording `{ id, version, source, integrity, files[], sharedEdits[] }` per installed capability; exports `readLedger`/`writeLedger`/`recordInstall`/`removeEntry`/`reconcile` (orphan detection)/`readSmallRegularFile` (utf8) + `readSmallRegularFileBuffer` (raw bytes, the byte-exact consent-hash reader, #1459 finding 1); atomic commit point and reconciliation basis for Phase-4 upgrade/remove | | `capability-lifecycle.cjs` | Capability lifecycle orchestration (ADR-1244 Phase 4, D5+D6) — composes the source resolver + ledger + trust gate into `installCapability`/`upgradeCapability`/`removeCapability`/`reconcileCapabilities`; ledger write is the commit point; upgrade is atomic stage-then-swap (old set aside, new swapped in, ledger committed, backup dropped) with deterministic crash recovery (`reconcileCapabilities` rolls forward/back to a fully-old-or-fully-new state); remove surgically strips only marker-stamped (`_gsdCapability`) shared-config entries, preserving user hand-edits; never executes capability code | -| `capability-loader.cjs` | Runtime Capability Registry overlay (ADR-1244 D2) — `loadRegistry({ includeInstalled })` composes the frozen first-party registry with a validated installed overlay read from `$GSD_HOME/.gsd/capabilities//` (global) and `/.gsd/capabilities//` (project); first-party-wins on id/skill/agent/config collisions, reserved-namespace rejection, load-time `engines.gsd` re-gate (skip-with-warning), and gate-kind fail-closed via `_overlay.blockedGates`; composes through the canonical `buildRegistry` so derived views never drift | +| `capability-loader.cjs` | Runtime Capability Registry overlay (ADR-1244 D2) — `loadRegistry({ includeInstalled })` composes the frozen first-party registry with a validated installed overlay read from `$GSD_HOME/.gsd/capabilities//` (global) and `/.gsd/capabilities//` (project); first-party-wins on id/skill/agent/config collisions, reserved-namespace rejection, load-time `engines.gsd` re-gate (skip-with-warning), and gate-kind fail-open via `_overlay.blockedGates` — a loud warning (stderr + envelope `warnings`) naming the load failure and `gsd capability remove ` remediation; no gate injected (#2009); composes through the canonical `buildRegistry` so derived views never drift | | `capability-registry.cjs` | Generated central Capability Registry — role-partitioned index of all co-located capability declarations (`capabilities//capability.json`); emitted by `scripts/gen-capability-registry.cjs --write` (ADR-894 §5) | | `capability-source.cjs` | Capability source resolver (ADR-1244 D3) — `resolveCapabilitySource(spec, opts)` fetches and stages a capability from local path, git (https/ssh/git transports only), npm pack (no lifecycle scripts), tarball (sha512 integrity verify before extraction), or registry (stub); tar-slip/symlink rejection; atomic staging to `$GSD_HOME/.gsd/capabilities//`; no capability code executes during install | | `capability-state.cjs` | Unified capability-state resolver (ADR-857 phase 4b/6) — composes install profile, runtime surface, and config activation into one per-capability view consumed by workflow hook rendering; exports pure `resolveCapabilityState`, reusable `resolveCapabilityRuntimeState`, and I/O handler `cmdCapabilityState`; command surface: `gsd-tools capability state [--config-dir ]` emitting `{ runtimeConfigDir, capabilities[] }` | diff --git a/docs/README.md b/docs/README.md index f05fce2cd..c78509ffa 100644 --- a/docs/README.md +++ b/docs/README.md @@ -72,7 +72,7 @@ Language versions: [English](README.md) · [Português (pt-BR)](pt-BR/README.md) - [Multi-agent orchestration](explanation/multi-agent-orchestration.md) — how subagents are spawned, scoped, and coordinated - [Security model](explanation/security-model.md) — trust boundaries, permissions, and safe automation - [The capability trust model](explanation/capability-trust-model.md) — why third-party capabilities are gated by consent + integrity + reversibility, not a sandbox -- [How overlay capabilities compose](explanation/capability-overlay-model.md) — why first-party always wins and how the loader resolves precedence, conflicts, and fail-closed gates +- [How overlay capabilities compose](explanation/capability-overlay-model.md) — why first-party always wins and how the loader resolves precedence, conflicts, and fail-open load-failure warnings - [Architecture](ARCHITECTURE.md) — system architecture, agent model, and data flow - [Discuss modes](workflow-discuss-mode.md) — assumptions mode vs interview mode for `/gsd-discuss-phase` - [Context monitoring](context-monitor.md) — context window monitoring hook architecture diff --git a/docs/explanation/capability-overlay-model.md b/docs/explanation/capability-overlay-model.md index a39326644..b1d95f956 100644 --- a/docs/explanation/capability-overlay-model.md +++ b/docs/explanation/capability-overlay-model.md @@ -181,9 +181,10 @@ candidate is processed. A single broken overlay cannot poison the rest of the se --- -## The one place where skipping is dangerous: gates +## The one place where a skip must be loud: gates -Skipping a broken overlay is the safe default for most surfaces — but not for *gates*. +Skipping a broken overlay is the safe default for every surface — including gates, though +gates get special treatment. A capability's loop hooks come in three kinds: @@ -195,21 +196,28 @@ For steps and contributions, skipping a capability means the loop simply proceed **without** that addition. That is *fail-open*, and it is correct: the loop is missing an optional step, not doing something unsafe. -A gate is the opposite. The whole purpose of a gate is to *stop* the loop when a -condition is not met — a deploy gate, a house-style verification gate, a safety check. If -GSD skipped a broken gate-declaring capability and proceeded, it would behave exactly as -if the gate had *passed* — silently waving through the very thing the gate existed to -block. That is a fail-open on a security-relevant control, and it is unacceptable. +A gate looks different at first glance. The whole purpose of a gate is to *stop* the loop +when a condition is not met — a deploy gate, a house-style verification gate, a safety +check. Silently skipping a broken gate-declaring capability and proceeding as if the gate +had *passed* would wave through the very thing the gate existed to block, with no signal +to the operator at all. -So composition treats gates asymmetrically from steps and contributions. When a -capability that declares a gate is skipped, GSD records its gate points in -`_overlay.incompatibleGateCapIds` and `_overlay.blockedGates`, and the loop resolver -**injects a synthetic blocking gate** at each of those extension points. The loop -**fails closed**: rather than proceed as if the gate passed, it halts with a message -naming the skipped capability and why its gate could not be evaluated. +So, per the maintainer decision on [#2009](https://github.com/open-gsd/gsd-core/issues/2009), +composition treats gates like steps and contributions for control flow — the loop always +proceeds — but never silently. When a capability that declares a gate is skipped, GSD +records its gate points in `_overlay.incompatibleGateCapIds` and `_overlay.blockedGates`, +and the loop resolver **injects no gate** at each of those extension points. The loop +**fails open**, but loudly: it emits a warning through two channels — stderr (the channel +host workflows/agents see when they run `gsd_run loop render-hooks `) and the +`loop render-hooks` JSON envelope's top-level `warnings` array. The warning names the +skipped capability, why it could not be loaded (for example, an incompatible +`engines.gsd` range), and the exact remediation — `gsd capability remove ` — so the +operator sees the missing control on every pass through the loop until they act on it, +instead of the loop halting project-wide over a single incompatible overlay. The discriminator is therefore *not* "is this overlay broken?" but "what does failing -to load it mean?" — and for a gate, failing to load it means you must not proceed. +to load it mean?" — and for a gate, failing to load it means the operator must be told, +unmistakably, until they resolve it. --- @@ -230,10 +238,12 @@ details make this safe rather than merely convenient: behind a path that a runtime dispatcher might `require()` a command module from. - Every dropped overlay's **gates are recorded as blocked** — using the same extraction as the per-candidate path — so a gate-declaring overlay that vanishes in the fallback - still **fails closed**, never open. + still **surfaces a loud warning** (stderr + envelope `warnings`) at its gate points + rather than vanishing silently (#2009). The principle is the same at every layer: when GSD cannot compose an overlay, it removes -the overlay's *additions* but never weakens a *control*. +the overlay's *additions* but never silences a *control* — a missing gate always +surfaces, even though, per #2009, it no longer blocks the loop. --- @@ -262,16 +272,20 @@ The overlay model rests on a few rules applied consistently: - **First-party always wins** every collision — id, skill/agent stem, config key, command family, reserved prefix. An overlay can only add, never override. - A bad overlay is **skipped, not crashed** — the loop always gets a usable registry. -- Skipping **fails open** for steps and contributions (a missing optional addition) but - **fails closed** for gates (a missing control must block, not pass). +- Skipping **fails open** for steps and contributions (a missing optional addition) and, + per [#2009](https://github.com/open-gsd/gsd-core/issues/2009), **fails open** for gates + too (a missing control, no gate injected) — but loudly, via a warning (stderr + the + envelope's `warnings` array) that names the load failure and its + `gsd capability remove ` remediation. - A whole-set compose failure **falls back to first-party**, clearing command roots and - still blocking dropped gates. + still surfacing dropped gates as loud warnings. - One canonical builder materialises both first-party and overlay views, so an accepted overlay has true parity with a shipped capability. Every one of these choices answers the same question — *what does it mean if this -composition step fails?* — and resolves it in favour of first-party authority and a -fail-closed security posture. +composition step fails?* — and resolves it in favour of first-party authority and, +per #2009, a loud fail-open posture: never silent, never a project-wide halt over a +single incompatible overlay. --- diff --git a/docs/how-to/configure-model-profiles.md b/docs/how-to/configure-model-profiles.md index 153611526..8d575d2f5 100644 --- a/docs/how-to/configure-model-profiles.md +++ b/docs/how-to/configure-model-profiles.md @@ -90,8 +90,9 @@ Phase types and their agents: | `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` | | `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` | | `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` | -| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` | -| `discuss`, `completion` | Reserved — no subagent today; accepted by schema for forward compatibility | +| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` | +| `discuss` | `gsd-assumptions-analyzer` | +| `completion` | Reserved — no subagent today; accepted by schema for forward compatibility | The `models` block accepts tier aliases only (`opus`, `sonnet`, `haiku`, `inherit`). For a fully-qualified model ID, use `model_overrides` per agent instead. diff --git a/docs/ja-JP/FEATURES.md b/docs/ja-JP/FEATURES.md index 31c1da45c..0e65982f6 100644 --- a/docs/ja-JP/FEATURES.md +++ b/docs/ja-JP/FEATURES.md @@ -2676,10 +2676,10 @@ capture_thought({ | スロット | 割り当てられたエージェント | |---------|----------------------| | `planning` | `gsd-planner`、`gsd-roadmapper`、`gsd-pattern-mapper` | -| `discuss` | (将来のサブエージェント用に予約) | +| `discuss` | `gsd-assumptions-analyzer` | | `research` | `gsd-phase-researcher`、`gsd-project-researcher`、`gsd-research-synthesizer`、`gsd-codebase-mapper`、`gsd-ui-researcher` | | `execution` | `gsd-executor`、`gsd-debugger`、`gsd-doc-writer` | -| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier` | +| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier`、`gsd-code-reviewer` | | `completion` | (将来のサブエージェント用に予約) | **受け入れられる値:** `"opus"` / `"sonnet"` / `"haiku"` / `"inherit"` diff --git a/docs/ja-JP/how-to/configure-model-profiles.md b/docs/ja-JP/how-to/configure-model-profiles.md index 2cb243c82..bd59d3f3b 100644 --- a/docs/ja-JP/how-to/configure-model-profiles.md +++ b/docs/ja-JP/how-to/configure-model-profiles.md @@ -88,8 +88,9 @@ npx @opengsd/gsd-core@latest --codex --global # または --opencode、--kilo | `planning` | `gsd-planner`、`gsd-roadmapper`、`gsd-pattern-mapper` | | `research` | `gsd-phase-researcher`、`gsd-project-researcher`、`gsd-research-synthesizer`、`gsd-codebase-mapper`、`gsd-ui-researcher` | | `execution` | `gsd-executor`、`gsd-debugger`、`gsd-doc-writer` | -| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier` | -| `discuss`、`completion` | 予約済み — 現在はサブエージェントなし。スキーマの前方互換性のために受け入れられます | +| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier`、`gsd-code-reviewer` | +| `discuss` | `gsd-assumptions-analyzer` | +| `completion` | 予約済み — 現在はサブエージェントなし。スキーマの前方互換性のために受け入れられます | `models` ブロックはティアエイリアス(`opus`、`sonnet`、`haiku`、`inherit`)のみを受け入れます。特定のエージェントに完全修飾のモデル ID を指定するには `model_overrides` を使用してください。 diff --git a/docs/ko-KR/how-to/configure-model-profiles.md b/docs/ko-KR/how-to/configure-model-profiles.md index c87349c3f..e76d3758c 100644 --- a/docs/ko-KR/how-to/configure-model-profiles.md +++ b/docs/ko-KR/how-to/configure-model-profiles.md @@ -88,8 +88,9 @@ npx @opengsd/gsd-core@latest --codex --global # 또는 --opencode, --kilo 등 | `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` | | `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` | | `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` | -| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` | -| `discuss`, `completion` | 예약됨 — 현재 서브에이전트 없음; 향후 호환성을 위해 스키마에서 허용 | +| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` | +| `discuss` | `gsd-assumptions-analyzer` | +| `completion` | 예약됨 — 현재 서브에이전트 없음; 향후 호환성을 위해 스키마에서 허용 | `models` 블록은 티어 별칭만 허용합니다(`opus`, `sonnet`, `haiku`, `inherit`). 완전히 정규화된 모델 ID는 에이전트별 `model_overrides`를 사용하세요. diff --git a/docs/pt-BR/CONFIGURATION.md b/docs/pt-BR/CONFIGURATION.md index 9510927f6..fda6c8b64 100644 --- a/docs/pt-BR/CONFIGURATION.md +++ b/docs/pt-BR/CONFIGURATION.md @@ -836,10 +836,10 @@ para que a alteração entre em vigor. Consulte a issue #2256. | Tipo de fase | Agentes | |---|---| | `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` | -| `discuss` | (reservado — sem subagente atualmente) | +| `discuss` | `gsd-assumptions-analyzer` | | `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` | | `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` | -| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` | +| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` | | `completion` | (reservado — sem subagente atualmente) | `discuss` e `completion` são aceitos pelo esquema para compatibilidade futura; defini-los hoje é um no-op até que um subagente seja mapeado para eles. diff --git a/docs/pt-BR/how-to/configure-model-profiles.md b/docs/pt-BR/how-to/configure-model-profiles.md index 66465a740..4f04d6b43 100644 --- a/docs/pt-BR/how-to/configure-model-profiles.md +++ b/docs/pt-BR/how-to/configure-model-profiles.md @@ -88,8 +88,9 @@ Tipos de fase e seus agentes: | `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` | | `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` | | `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` | -| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` | -| `discuss`, `completion` | Reservado — nenhum subagente hoje; aceito pelo esquema para compatibilidade futura | +| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` | +| `discuss` | `gsd-assumptions-analyzer` | +| `completion` | Reservado — nenhum subagente hoje; aceito pelo esquema para compatibilidade futura | O bloco `models` aceita apenas aliases de nível (`opus`, `sonnet`, `haiku`, `inherit`). Para um ID de modelo totalmente qualificado, use `model_overrides` por agente. diff --git a/docs/zh-CN/CONFIGURATION.md b/docs/zh-CN/CONFIGURATION.md index 2b49cf333..a43d6ba14 100644 --- a/docs/zh-CN/CONFIGURATION.md +++ b/docs/zh-CN/CONFIGURATION.md @@ -805,10 +805,10 @@ gsd-tools query config-set features.thinking_partner false | 阶段类型 | Agents | |---|---| | `planning` | `gsd-planner`, `gsd-roadmapper`, `gsd-pattern-mapper` | -| `discuss` | (保留——当前无 subagent) | +| `discuss` | `gsd-assumptions-analyzer` | | `research` | `gsd-phase-researcher`, `gsd-project-researcher`, `gsd-research-synthesizer`, `gsd-codebase-mapper`, `gsd-ui-researcher` | | `execution` | `gsd-executor`, `gsd-debugger`, `gsd-doc-writer` | -| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier` | +| `verification` | `gsd-verifier`, `gsd-plan-checker`, `gsd-integration-checker`, `gsd-nyquist-auditor`, `gsd-ui-checker`, `gsd-ui-auditor`, `gsd-doc-verifier`, `gsd-code-reviewer` | | `completion` | (保留——当前无 subagent) | `discuss` 和 `completion` 被 schema 接受以保持前向兼容性;今天设置它们是无操作,直到某个 subagent 映射到它们为止。 diff --git a/docs/zh-CN/FEATURES.md b/docs/zh-CN/FEATURES.md index 80953d3af..3d9e87e08 100644 --- a/docs/zh-CN/FEATURES.md +++ b/docs/zh-CN/FEATURES.md @@ -2692,10 +2692,10 @@ capture_thought({ | 槽位 | 分配的智能体 | |------|-----------------| | `planning` | `gsd-planner`、`gsd-roadmapper`、`gsd-pattern-mapper` | -| `discuss` | (为未来子智能体保留) | +| `discuss` | `gsd-assumptions-analyzer` | | `research` | `gsd-phase-researcher`、`gsd-project-researcher`、`gsd-research-synthesizer`、`gsd-codebase-mapper`、`gsd-ui-researcher` | | `execution` | `gsd-executor`、`gsd-debugger`、`gsd-doc-writer` | -| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier` | +| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier`、`gsd-code-reviewer` | | `completion` | (为未来子智能体保留) | **接受的值:** `"opus"` / `"sonnet"` / `"haiku"` / `"inherit"` diff --git a/docs/zh-CN/how-to/configure-model-profiles.md b/docs/zh-CN/how-to/configure-model-profiles.md index f3e790985..2bb89ae40 100644 --- a/docs/zh-CN/how-to/configure-model-profiles.md +++ b/docs/zh-CN/how-to/configure-model-profiles.md @@ -88,8 +88,9 @@ npx @opengsd/gsd-core@latest --codex --global # or --opencode, --kilo, etc. | `planning` | `gsd-planner`、`gsd-roadmapper`、`gsd-pattern-mapper` | | `research` | `gsd-phase-researcher`、`gsd-project-researcher`、`gsd-research-synthesizer`、`gsd-codebase-mapper`、`gsd-ui-researcher` | | `execution` | `gsd-executor`、`gsd-debugger`、`gsd-doc-writer` | -| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier` | -| `discuss`、`completion` | 保留——目前无子代理;已被模式接受以备向后兼容 | +| `verification` | `gsd-verifier`、`gsd-plan-checker`、`gsd-integration-checker`、`gsd-nyquist-auditor`、`gsd-ui-checker`、`gsd-ui-auditor`、`gsd-doc-verifier`、`gsd-code-reviewer` | +| `discuss` | `gsd-assumptions-analyzer` | +| `completion` | 保留——目前无子代理;已被模式接受以备向后兼容 | `models` 块仅接受层级别名(`opus`、`sonnet`、`haiku`、`inherit`)。如需使用完全限定的模型 ID,请改用按代理设置的 `model_overrides`。 diff --git a/gsd-core/references/model-profiles.md b/gsd-core/references/model-profiles.md index 52e3f7a8d..7cdefdbc8 100644 --- a/gsd-core/references/model-profiles.md +++ b/gsd-core/references/model-profiles.md @@ -45,10 +45,10 @@ Model profiles control which Claude model each GSD agent uses. This allows balan | Phase type | Agents | |---|---| | `planning` | gsd-planner, gsd-roadmapper, gsd-pattern-mapper | -| `discuss` | (reserved — no subagent today) | +| `discuss` | `gsd-assumptions-analyzer` | | `research` | gsd-phase-researcher, gsd-project-researcher, gsd-research-synthesizer, gsd-codebase-mapper, gsd-ui-researcher | | `execution` | gsd-executor, gsd-debugger, gsd-doc-writer | -| `verification` | gsd-verifier, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier | +| `verification` | gsd-verifier, gsd-plan-checker, gsd-integration-checker, gsd-nyquist-auditor, gsd-ui-checker, gsd-ui-auditor, gsd-doc-verifier, gsd-code-reviewer | | `completion` | (reserved — no subagent today) | ### Resolution precedence (highest to lowest) diff --git a/gsd-core/workflows/code-review-fix.md b/gsd-core/workflows/code-review-fix.md index 9a14680b6..833be2013 100644 --- a/gsd-core/workflows/code-review-fix.md +++ b/gsd-core/workflows/code-review-fix.md @@ -23,6 +23,10 @@ INIT=$(gsd_run query init.phase-op "${PHASE_ARG}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS_FIXER=$(gsd_run query agent-skills gsd-code-fixer) AGENT_SKILLS_REVIEWER=$(gsd_run query agent-skills gsd-code-reviewer) +# #2072: resolve the routed models so model_overrides / models. are honored +# (gsd-code-reviewer → "verification", gsd-code-fixer → "execution"); thread them below. +REVIEWER_MODEL=$(gsd_run query resolve-model gsd-code-reviewer --raw) +FIXER_MODEL=$(gsd_run query resolve-model gsd-code-fixer --raw) ``` Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`. @@ -194,7 +198,7 @@ echo "Fix scope: ${FIX_SCOPE}" Use Agent() to spawn agent: ```text -Agent(subagent_type="gsd-code-fixer", prompt=" +Agent(subagent_type="gsd-code-fixer", model="{FIXER_MODEL}", prompt=" ${REVIEW_PATH} @@ -277,7 +281,7 @@ if [ "$AUTO_MODE" = "true" ]; then # Spawn gsd-code-reviewer agent to re-review (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze) # (This overwrites REVIEW_PATH with latest review state) - Agent(subagent_type="gsd-code-reviewer", prompt=" + Agent(subagent_type="gsd-code-reviewer", model="{REVIEWER_MODEL}", prompt=" depth: ${REVIEW_DEPTH} phase_dir: ${PHASE_DIR} @@ -311,7 +315,7 @@ ${AGENT_SKILLS_REVIEWER}") # Still has issues — spawn fixer again (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze) echo "Issues remain. Applying fixes for iteration ${ITERATION}..." - Agent(subagent_type="gsd-code-fixer", prompt=" + Agent(subagent_type="gsd-code-fixer", model="{FIXER_MODEL}", prompt=" ${REVIEW_PATH} diff --git a/gsd-core/workflows/code-review.md b/gsd-core/workflows/code-review.md index 0a257473f..a559cd74a 100644 --- a/gsd-core/workflows/code-review.md +++ b/gsd-core/workflows/code-review.md @@ -22,6 +22,9 @@ PHASE_ARG="${1}" INIT=$(gsd_run query init.phase-op "${PHASE_ARG}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS_REVIEWER=$(gsd_run query agent-skills gsd-code-reviewer) +# #2072: resolve the routed model so model_overrides / models.verification are honored +# (the resolver maps gsd-code-reviewer → phaseType "verification"); thread it below. +REVIEWER_MODEL=$(gsd_run query resolve-model gsd-code-reviewer --raw) ``` Parse from init JSON: `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `padded_phase`, `commit_docs`. @@ -482,7 +485,7 @@ Spawn the gsd-code-reviewer agent: Print: `◆ Spawning code reviewer... (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)` ``` -Agent(subagent_type="gsd-code-reviewer", prompt=" +Agent(subagent_type="gsd-code-reviewer", model="{REVIEWER_MODEL}", prompt=" ${FILES_TO_READ} diff --git a/gsd-core/workflows/discuss-phase-assumptions.md b/gsd-core/workflows/discuss-phase-assumptions.md index 80c6666ae..6aa600c70 100644 --- a/gsd-core/workflows/discuss-phase-assumptions.md +++ b/gsd-core/workflows/discuss-phase-assumptions.md @@ -68,6 +68,9 @@ _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-pars INIT=$(gsd_run query init.phase-op "${PHASE}") if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS_ANALYZER=$(gsd_run query agent-skills gsd-assumptions-analyzer) +# #2072: resolve the routed model so model_overrides / models.discuss are honored +# (the resolver maps gsd-assumptions-analyzer → phaseType "discuss"); thread it below. +ANALYZER_MODEL=$(gsd_run query resolve-model gsd-assumptions-analyzer --raw) ``` Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, @@ -255,7 +258,7 @@ If no USER-PROFILE.md: calibration_tier = "standard" **Spawn Explore subagent** (runs in a subagent — no output until it returns, ~1–5 min; expected, not a freeze)**:** ``` -Agent(subagent_type="gsd-assumptions-analyzer", prompt=""" +Agent(subagent_type="gsd-assumptions-analyzer", model="{ANALYZER_MODEL}", prompt=""" Analyze the codebase for Phase {PHASE}: {phase_name}. Phase goal: {roadmap_description} diff --git a/gsd-core/workflows/quick.md b/gsd-core/workflows/quick.md index fc9f98756..b3ec7b140 100644 --- a/gsd-core/workflows/quick.md +++ b/gsd-core/workflows/quick.md @@ -134,7 +134,7 @@ AGENT_SKILLS_CHECKER=$(gsd_run query agent-skills gsd-plan-checker) AGENT_SKILLS_VERIFIER=$(gsd_run query agent-skills gsd-verifier) ``` -Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`. +Parse JSON for: `planner_model`, `executor_model`, `checker_model`, `verifier_model`, `reviewer_model`, `commit_docs`, `branch_name`, `quick_id`, `slug`, `date`, `timestamp`, `quick_dir`, `task_dir`, `roadmap_exists`, `planning_exists`. ```bash USE_WORKTREES=$(gsd_run query config-get workflow.use_worktrees --raw 2>/dev/null || echo "true") @@ -868,7 +868,7 @@ Agent( Output: ${QUICK_DIR}/${quick_id}-REVIEW.md Depth: quick", subagent_type="gsd-code-reviewer", - model="{executor_model}" + model="{reviewer_model}" ) ``` diff --git a/src/capability-loader.cts b/src/capability-loader.cts index 673570770..2ba118ccc 100644 --- a/src/capability-loader.cts +++ b/src/capability-loader.cts @@ -17,10 +17,12 @@ * `gsd-core-` / `anthropic-` id prefix) is rejected. * - Load-time re-gate (default-resilient): an overlay that fails validation or * whose `engines.gsd` does not satisfy the running GSD version is SKIPPED - * with a warning — it never crashes the loop. EXCEPTION (per-hook-kind - * policy): a skipped capability that declares a `gate` is recorded in - * `_overlay.incompatibleGateCapIds` so the loop resolver can fail CLOSED for - * that gate rather than silently proceeding as if it had passed. + * with a warning — it never crashes the loop. A skipped capability that + * declares a `gate` is additionally recorded in + * `_overlay.incompatibleGateCapIds` / `_overlay.blockedGates` so the loop + * resolver can surface a loud fail-OPEN advisory for that gate (#2009): the + * un-evaluable gate is skipped (not enforced) with a remediation message, + * rather than silently vanishing. * * The merged registry is materialized by the canonical `buildRegistry` * (re-exported from the generator, which ships) over a cap-map reconstructed @@ -845,12 +847,12 @@ export function loadRegistry(options: LoadRegistryOptions = {}): Registry { // to load. Clear the map (the first-party base never lists overlay commandRoots — first-party // command modules ship in bin/lib/, not via _overlay.commandRoots). meta.commandRoots = {}; - // #1461 OVL-2 fail-CLOSED on compose failure (HIGH): the fallback DROPS every accepted overlay, - // so any accepted overlay that DECLARED a gate would have its gate silently vanish → a blocking - // gate FAILS OPEN, violating ADR-1244 (a skipped capability declaring a gate must FAIL CLOSED). + // #1461 OVL-2 (HIGH): on compose failure the fallback DROPS every accepted overlay, so any + // accepted overlay that DECLARED a gate would have its gate silently vanish with no trace. // Record each dropped gate-declaring overlay's gate as blocked using the SAME extraction the - // per-candidate `skip()` closure uses (gatePointsOf), so loop-resolver injects the synthetic - // blocking gate at each declared point exactly as it would for a per-candidate skip. + // per-candidate `skip()` closure uses (gatePointsOf), so loop-resolver surfaces the loud + // fail-OPEN advisory (#2009) at each declared point exactly as it would for a per-candidate + // skip — the gate does not silently disappear. for (const cap of overlayCaps) { const gatePoints = gatePointsOf(cap); if (gatePoints.length === 0) continue; diff --git a/src/init.cts b/src/init.cts index 8ac092f2c..196b266e2 100644 --- a/src/init.cts +++ b/src/init.cts @@ -759,6 +759,9 @@ function cmdInitQuick(cwd: string, description: string | undefined, raw: boolean executor_model: resolveModelInternal(cwd, 'gsd-executor'), checker_model: resolveModelInternal(cwd, 'gsd-plan-checker'), verifier_model: resolveModelInternal(cwd, 'gsd-verifier'), + // #2072: the quick review step spawns gsd-code-reviewer; resolve its own model + // so model_overrides / models.verification apply (was reusing executor_model). + reviewer_model: resolveModelInternal(cwd, 'gsd-code-reviewer'), commit_docs: config.commit_docs, branch_name: quickBranchName, diff --git a/src/loop-resolver.cts b/src/loop-resolver.cts index 1b0d3b1aa..ff86175a7 100644 --- a/src/loop-resolver.cts +++ b/src/loop-resolver.cts @@ -454,6 +454,27 @@ function renderLoopHooks(resolved: ResolveLoopHooksResult): string { * Missing value → coreError + non-zero exit. * Unknown/inactive capId → `false` (not an error). */ +// #2009: a capability id surfaced inside the runnable `gsd capability remove ` +// remediation must match the canonical kebab-case id shape (identical to +// capability-consent.cts / capability-ledger.cts) before it is embedded — a raw +// overlay directory name is attacker-controlled and can carry shell/markdown +// metacharacters (backticks, ';', '|', '$()'). An id that fails this check is +// withheld and no runnable command is rendered for it. +const LOAD_FAIL_CAP_ID_RE = /^[a-z][a-z0-9-]*$/; + +// #2009: neutralize control chars, newlines, and backticks from a third-party +// load-failure reason so a malicious manifest cannot break out of the warning +// line or inject markdown / prompt content into the surfaced message. +function sanitizeLoadFailReason(reason: unknown): string { + const cleaned = String(reason) + // Strip C0 control chars, DEL, and backticks; collapse remaining whitespace. + .replace(/[\x00-\x1F\x7F`]/g, ' ') + .replace(/\s+/g, " ") + .trim() + .slice(0, 300); + return cleaned || '(no reason given)'; +} + function cmdLoopRenderHooks( cwd: string, point: string, @@ -519,26 +540,55 @@ function cmdLoopRenderHooks( return; } - // ── ADR-1244 D2 fail-closed gate injection ──────────────────────────────────── - // For every skipped overlay capability that declared a gate at this point, - // inject a synthetic BLOCKING gate into the resolved output so the loop HALTS - // rather than silently proceeding as if the gate had passed. step/contribution - // overlays that were skipped are left open (skip-open is correct for them). + // ── ADR-1244 D2: load-failed capability gates FAIL OPEN with a loud warning ──── + // Decision (#2009): a capability that failed to LOAD must not block the loop. + // The prior behavior injected a BLOCKING synthetic gate (blocking:true, + // onError:'halt') at every point where the skipped cap declared a gate, so a + // single incompatible capability halted every ship:pre / verify:post + // project-wide for a load error unrelated to what the gate checked — with no + // remediation surfaced. We now fail OPEN: no gate is injected (the loop proceeds + // and `--active-cap ` correctly reports it inactive), and a loud + // warning is emitted instead — to STDERR (which the operator, or the agent + // running the command, actually sees regardless of how the host workflow + // consumes stdout) AND in the envelope's `warnings` channel for structured + // consumers. The warning names the load reason and the exact + // `gsd capability remove ` remediation so the operator can clear the broken + // capability. blockedGates is still recorded by the loader; only the consequence + // changes from block to warn. step/contribution overlays were already skip-open. + // + // The gate injection was dropped rather than made non-blocking because no host + // workflow generically surfaces an arbitrary gate's message at ship:pre / + // verify:post (consumers dispatch on specific capIds / ref.skills), and the + // generic gate consumers expect an object-shaped `check`, not a prose string — + // so an injected advisory gate would be silently dropped or mis-dispatched. A + // stderr warning is the channel that is actually surfaced. (See #2009 review.) const overlayMeta = (registry as { _overlay?: { blockedGates?: Array<{ point: string; capId: string; reason: string }> } })['_overlay']; + const loadFailWarnings: string[] = []; if (overlayMeta && Array.isArray(overlayMeta.blockedGates)) { for (const blocked of overlayMeta.blockedGates) { - if (blocked.point === point) { - const syntheticGate: ActiveHook = { - capId: blocked.capId, - kind: 'gate', - blocking: true, - onError: 'halt', - check: `capability "${blocked.capId}" was skipped at load (${blocked.reason}); its gate at ${point} cannot be evaluated — failing closed`, - }; - resolved.activeHooks.push(syntheticGate); - } + if (blocked.point !== point) continue; + // Security (#2009 review): capId/reason come from a third-party manifest or + // directory name. Validate capId before embedding it in the runnable + // remediation command; withhold it (no runnable command) if it is not a + // canonical id. Strip control chars/backticks from reason. + const idValid = LOAD_FAIL_CAP_ID_RE.test(String(blocked.capId)); + const capLabel = idValid + ? `"${blocked.capId}"` + : 'with an invalid id (withheld) under .gsd/capabilities/'; + const remediation = idValid + ? `Run \`gsd capability remove ${blocked.capId}\` to remove it, or fix the load error.` + : 'Remove the offending capability directory under .gsd/capabilities/, or fix the load error.'; + loadFailWarnings.push( + `capability ${capLabel} failed to load (${sanitizeLoadFailReason(blocked.reason)}); ` + + `its gate at ${point} is SKIPPED and NOT enforced (failing open). ${remediation}`, + ); } } + // Emit loudly to stderr in EVERY output mode (including --active-cap), so a + // skipped gate is never silently invisible to the operator/agent. + for (const w of loadFailWarnings) { + process.stderr.write(`gsd: warning — ${w}\n`); + } // --active-cap mode: print exactly 'true' or 'false' with no envelope if (activeCapId !== undefined) { @@ -558,8 +608,12 @@ function cmdLoopRenderHooks( activeHooks: resolved.activeHooks, rendered, }; - if (state.warnings && state.warnings.length > 0) { - envelope.warnings = state.warnings; + // Surface capability-state warnings and the #2009 load-failure fail-open + // warnings together in the structured `warnings` channel (in addition to the + // stderr emission above, which is the channel host workflows actually see). + const combinedWarnings = [...(state.warnings || []), ...loadFailWarnings]; + if (combinedWarnings.length > 0) { + envelope.warnings = combinedWarnings; } coreOutput(envelope, raw); diff --git a/tests/fixtures/golden-install-parity/antigravity.json b/tests/fixtures/golden-install-parity/antigravity.json index e6c8252c3..e7e4e189d 100644 --- a/tests/fixtures/golden-install-parity/antigravity.json +++ b/tests/fixtures/golden-install-parity/antigravity.json @@ -94,7 +94,7 @@ "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-profiles.md": "d6ed560db6357ad2", + "gsd-core/references/model-profiles.md": "6568ca29b6ee00d8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8", @@ -207,13 +207,13 @@ "gsd-core/workflows/autonomous.md": "603ce2019835f00e", "gsd-core/workflows/check-todos.md": "5a62092df800ad7c", "gsd-core/workflows/cleanup.md": "12c8fd85d3010fe6", - "gsd-core/workflows/code-review-fix.md": "5738cb929c995008", - "gsd-core/workflows/code-review.md": "d454365f1704d0bc", + "gsd-core/workflows/code-review-fix.md": "60640e633b0a124b", + "gsd-core/workflows/code-review.md": "5c40505c01871153", "gsd-core/workflows/complete-milestone.md": "aaf272074acec69d", "gsd-core/workflows/debug.md": "68f1ddc74886ebe5", "gsd-core/workflows/diagnose-issues.md": "c8c41993c277363c", "gsd-core/workflows/discovery-phase.md": "3de990caffdde4f8", - "gsd-core/workflows/discuss-phase-assumptions.md": "42210a0cbe1bac70", + "gsd-core/workflows/discuss-phase-assumptions.md": "8581fd77def7ce84", "gsd-core/workflows/discuss-phase-power.md": "5c0d1ca6abda0383", "gsd-core/workflows/discuss-phase.md": "c0a977154470b7f5", "gsd-core/workflows/discuss-phase/modes/advisor.md": "ab0c68941386998b", @@ -279,7 +279,7 @@ "gsd-core/workflows/pr-branch.md": "dc5598ae8accdecd", "gsd-core/workflows/profile-user.md": "355af92ac285567f", "gsd-core/workflows/progress.md": "79a11ce798082054", - "gsd-core/workflows/quick.md": "35582887917ef938", + "gsd-core/workflows/quick.md": "7108075f69e88e7c", "gsd-core/workflows/reapply-patches.md": "4dcd6117d0a507ca", "gsd-core/workflows/remove-phase.md": "23b9eb0858a2535e", "gsd-core/workflows/remove-workspace.md": "d0bd7e0601138798", diff --git a/tests/fixtures/golden-install-parity/augment.json b/tests/fixtures/golden-install-parity/augment.json index 7785d1737..9ced83650 100644 --- a/tests/fixtures/golden-install-parity/augment.json +++ b/tests/fixtures/golden-install-parity/augment.json @@ -165,7 +165,7 @@ "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-profiles.md": "8484ad9799b7f680", + "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -278,13 +278,13 @@ "gsd-core/workflows/autonomous.md": "7e5683728ef33707", "gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc", "gsd-core/workflows/cleanup.md": "0daa2f2720c111f1", - "gsd-core/workflows/code-review-fix.md": "c323f7098b671bd6", - "gsd-core/workflows/code-review.md": "ad450c6ef8459dad", + "gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075", + "gsd-core/workflows/code-review.md": "334c90c401f291f8", "gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2", "gsd-core/workflows/debug.md": "849d7e5b9c12dcae", "gsd-core/workflows/diagnose-issues.md": "6cc3900891dfb927", "gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761", - "gsd-core/workflows/discuss-phase-assumptions.md": "1e229f6d21831d60", + "gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88", "gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a", "gsd-core/workflows/discuss-phase.md": "5c5e8d8c2c9d95aa", "gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653", @@ -350,7 +350,7 @@ "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", "gsd-core/workflows/profile-user.md": "14263db831230142", "gsd-core/workflows/progress.md": "893aa3c36983f74b", - "gsd-core/workflows/quick.md": "5790ceb09aa685be", + "gsd-core/workflows/quick.md": "069fe37d083a94a3", "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", diff --git a/tests/fixtures/golden-install-parity/claude.json b/tests/fixtures/golden-install-parity/claude.json index 2be2052f2..2fb838fa0 100644 --- a/tests/fixtures/golden-install-parity/claude.json +++ b/tests/fixtures/golden-install-parity/claude.json @@ -93,7 +93,7 @@ "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-profiles.md": "90e2c9bc577278fd", + "gsd-core/references/model-profiles.md": "c249163663bbea53", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -206,13 +206,13 @@ "gsd-core/workflows/autonomous.md": "722397c04272dfaa", "gsd-core/workflows/check-todos.md": "6c2a43d1d3e86589", "gsd-core/workflows/cleanup.md": "0daa2f2720c111f1", - "gsd-core/workflows/code-review-fix.md": "adb62c695b39ef61", - "gsd-core/workflows/code-review.md": "6c40b95e799907d0", + "gsd-core/workflows/code-review-fix.md": "722416b71b31c5fc", + "gsd-core/workflows/code-review.md": "506412604f767adc", "gsd-core/workflows/complete-milestone.md": "dcf1182398efb1ca", "gsd-core/workflows/debug.md": "a9397fd35cca2240", "gsd-core/workflows/diagnose-issues.md": "75ffc381ac3059ff", "gsd-core/workflows/discovery-phase.md": "6161c60d752d0058", - "gsd-core/workflows/discuss-phase-assumptions.md": "46876f83547db40a", + "gsd-core/workflows/discuss-phase-assumptions.md": "a8cd1db094fefd35", "gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6", "gsd-core/workflows/discuss-phase.md": "df56c8cd170b2150", "gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653", @@ -278,7 +278,7 @@ "gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd", "gsd-core/workflows/profile-user.md": "ff3820a27731ceb8", "gsd-core/workflows/progress.md": "bd1ecf9207331bda", - "gsd-core/workflows/quick.md": "da83b1a15b7f1bf7", + "gsd-core/workflows/quick.md": "3363bdfefd403686", "gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041", "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "f3ab3a88a7e9e1ed", diff --git a/tests/fixtures/golden-install-parity/cline.json b/tests/fixtures/golden-install-parity/cline.json index f3841bb20..2851df9ba 100644 --- a/tests/fixtures/golden-install-parity/cline.json +++ b/tests/fixtures/golden-install-parity/cline.json @@ -97,7 +97,7 @@ "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-profiles.md": "eb144b83e31196b8", + "gsd-core/references/model-profiles.md": "1794ad3d9854129e", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -210,13 +210,13 @@ "gsd-core/workflows/autonomous.md": "cc5217b1b2238a4c", "gsd-core/workflows/check-todos.md": "32fdf33f5dc8bdde", "gsd-core/workflows/cleanup.md": "b0ebfc48792b407b", - "gsd-core/workflows/code-review-fix.md": "b946fe7bcb303852", - "gsd-core/workflows/code-review.md": "0b24efcfa71a2ed2", + "gsd-core/workflows/code-review-fix.md": "e4549af672e74e6f", + "gsd-core/workflows/code-review.md": "a65e3e869508f89e", "gsd-core/workflows/complete-milestone.md": "c0808127038a8f86", "gsd-core/workflows/debug.md": "f42e8e3cbc298694", "gsd-core/workflows/diagnose-issues.md": "e616d0d730328d68", "gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761", - "gsd-core/workflows/discuss-phase-assumptions.md": "89987fd183e0d552", + "gsd-core/workflows/discuss-phase-assumptions.md": "9efcb2ef6a9245b3", "gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a", "gsd-core/workflows/discuss-phase.md": "a290d5ee19607bb7", "gsd-core/workflows/discuss-phase/modes/advisor.md": "250de9aae90daebc", @@ -282,7 +282,7 @@ "gsd-core/workflows/pr-branch.md": "9923878a4f6a2d91", "gsd-core/workflows/profile-user.md": "26f74db0a7fcd268", "gsd-core/workflows/progress.md": "9f326d63afb4b76b", - "gsd-core/workflows/quick.md": "68a9dfcd53f0859f", + "gsd-core/workflows/quick.md": "2afc046e94daad0a", "gsd-core/workflows/reapply-patches.md": "eb4272145a117904", "gsd-core/workflows/remove-phase.md": "e336350f8113a328", "gsd-core/workflows/remove-workspace.md": "e685dfbd736dfd90", diff --git a/tests/fixtures/golden-install-parity/codebuddy.json b/tests/fixtures/golden-install-parity/codebuddy.json index df78da0b6..5ea53f832 100644 --- a/tests/fixtures/golden-install-parity/codebuddy.json +++ b/tests/fixtures/golden-install-parity/codebuddy.json @@ -165,7 +165,7 @@ "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-profiles.md": "8484ad9799b7f680", + "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -278,13 +278,13 @@ "gsd-core/workflows/autonomous.md": "7e5683728ef33707", "gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc", "gsd-core/workflows/cleanup.md": "0daa2f2720c111f1", - "gsd-core/workflows/code-review-fix.md": "c323f7098b671bd6", - "gsd-core/workflows/code-review.md": "ad450c6ef8459dad", + "gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075", + "gsd-core/workflows/code-review.md": "334c90c401f291f8", "gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2", "gsd-core/workflows/debug.md": "849d7e5b9c12dcae", "gsd-core/workflows/diagnose-issues.md": "77d98ac07c4a26ff", "gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761", - "gsd-core/workflows/discuss-phase-assumptions.md": "1e229f6d21831d60", + "gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88", "gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a", "gsd-core/workflows/discuss-phase.md": "02a5381c9a2173be", "gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653", @@ -350,7 +350,7 @@ "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", "gsd-core/workflows/profile-user.md": "4fa910d15dea5695", "gsd-core/workflows/progress.md": "893aa3c36983f74b", - "gsd-core/workflows/quick.md": "9033dbe58443af36", + "gsd-core/workflows/quick.md": "cc530299ba461539", "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", diff --git a/tests/fixtures/golden-install-parity/codex.json b/tests/fixtures/golden-install-parity/codex.json index a6e01217e..4d07225df 100644 --- a/tests/fixtures/golden-install-parity/codex.json +++ b/tests/fixtures/golden-install-parity/codex.json @@ -129,7 +129,7 @@ "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-profiles.md": "86e81f17c2f23fff", + "gsd-core/references/model-profiles.md": "bd90862f68007f2e", "gsd-core/references/mvp-concepts.md": "23201c8118fb074a", "gsd-core/references/phase-argument-parsing.md": "531176f66da49c98", "gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8", @@ -242,13 +242,13 @@ "gsd-core/workflows/autonomous.md": "92f08626d4aea668", "gsd-core/workflows/check-todos.md": "b2b103e8638e760a", "gsd-core/workflows/cleanup.md": "5d48d64664222a3e", - "gsd-core/workflows/code-review-fix.md": "3ff5f7eac2269ca3", - "gsd-core/workflows/code-review.md": "82858ab037b9176e", + "gsd-core/workflows/code-review-fix.md": "ae7f9c6b39a23c12", + "gsd-core/workflows/code-review.md": "eadada9e0a89adf2", "gsd-core/workflows/complete-milestone.md": "017df7443bd08da5", "gsd-core/workflows/debug.md": "cc7b2d2fd4307a78", "gsd-core/workflows/diagnose-issues.md": "e38bb21d06dff077", "gsd-core/workflows/discovery-phase.md": "71a4b78ff876a854", - "gsd-core/workflows/discuss-phase-assumptions.md": "f23abfe76623ed63", + "gsd-core/workflows/discuss-phase-assumptions.md": "0d936ec25299917c", "gsd-core/workflows/discuss-phase-power.md": "3f8b83dc6be2e9d5", "gsd-core/workflows/discuss-phase.md": "7c9df6656b81fda9", "gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653", @@ -314,7 +314,7 @@ "gsd-core/workflows/pr-branch.md": "d13e1cc81de40896", "gsd-core/workflows/profile-user.md": "05828c8cc61ef384", "gsd-core/workflows/progress.md": "7bedc431bb55edb8", - "gsd-core/workflows/quick.md": "3e7686705da2af19", + "gsd-core/workflows/quick.md": "62b9e138dc70b479", "gsd-core/workflows/reapply-patches.md": "26297b84736e66a4", "gsd-core/workflows/remove-phase.md": "9ee0fddd11a0d9d4", "gsd-core/workflows/remove-workspace.md": "19d7465aaa50cb62", diff --git a/tests/fixtures/golden-install-parity/copilot.json b/tests/fixtures/golden-install-parity/copilot.json index 060f81cd9..68c1c364d 100644 --- a/tests/fixtures/golden-install-parity/copilot.json +++ b/tests/fixtures/golden-install-parity/copilot.json @@ -95,7 +95,7 @@ "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-profiles.md": "d6ed560db6357ad2", + "gsd-core/references/model-profiles.md": "6568ca29b6ee00d8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8", @@ -208,13 +208,13 @@ "gsd-core/workflows/autonomous.md": "dd972ddde9663295", "gsd-core/workflows/check-todos.md": "be9b50b5f28d7504", "gsd-core/workflows/cleanup.md": "8233b05ebf011bec", - "gsd-core/workflows/code-review-fix.md": "ab2b467b41522c10", - "gsd-core/workflows/code-review.md": "1bbbe61f45fccf4c", + "gsd-core/workflows/code-review-fix.md": "fda53892ae4b17fc", + "gsd-core/workflows/code-review.md": "f1c045ec4d33abc8", "gsd-core/workflows/complete-milestone.md": "470cf39261400ee2", "gsd-core/workflows/debug.md": "36cb536be452d79e", "gsd-core/workflows/diagnose-issues.md": "42acbe2a43fc886e", "gsd-core/workflows/discovery-phase.md": "8e99da61fb2b7074", - "gsd-core/workflows/discuss-phase-assumptions.md": "ab2d26b194805ac1", + "gsd-core/workflows/discuss-phase-assumptions.md": "ab0c432b84038681", "gsd-core/workflows/discuss-phase-power.md": "5c0d1ca6abda0383", "gsd-core/workflows/discuss-phase.md": "31a79ea11c1cdd5f", "gsd-core/workflows/discuss-phase/modes/advisor.md": "654cdaf1138d5e27", @@ -280,7 +280,7 @@ "gsd-core/workflows/pr-branch.md": "2833905f119b5722", "gsd-core/workflows/profile-user.md": "5cc032206c99ef71", "gsd-core/workflows/progress.md": "6b9a84a43dc55af5", - "gsd-core/workflows/quick.md": "7ddfe17f048541ec", + "gsd-core/workflows/quick.md": "eb68ea8748546331", "gsd-core/workflows/reapply-patches.md": "8fd59e24b486f180", "gsd-core/workflows/remove-phase.md": "e262654e319d1bc4", "gsd-core/workflows/remove-workspace.md": "ceddfeef5f2d6754", diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index c618c1846..4ed920084 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -165,7 +165,7 @@ "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-profiles.md": "f4c013e700c52b08", + "gsd-core/references/model-profiles.md": "5452b2e19e19f77e", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e562947d1bf5c5a4", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -278,13 +278,13 @@ "gsd-core/workflows/autonomous.md": "4b20eda9a0b587ce", "gsd-core/workflows/check-todos.md": "1a67337d1630848f", "gsd-core/workflows/cleanup.md": "db47963f103c2748", - "gsd-core/workflows/code-review-fix.md": "49e7f024c551b3a5", - "gsd-core/workflows/code-review.md": "6a9fd2996a6b600e", + "gsd-core/workflows/code-review-fix.md": "c57af378033b1b58", + "gsd-core/workflows/code-review.md": "32c37bcbec8b8698", "gsd-core/workflows/complete-milestone.md": "1400a4856f592f3e", "gsd-core/workflows/debug.md": "a73d8018986131ba", "gsd-core/workflows/diagnose-issues.md": "cd582747131726e3", "gsd-core/workflows/discovery-phase.md": "7dcf150998559c11", - "gsd-core/workflows/discuss-phase-assumptions.md": "373788c6b7eab272", + "gsd-core/workflows/discuss-phase-assumptions.md": "c25c6a6c633d71b6", "gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6", "gsd-core/workflows/discuss-phase.md": "14688ff19ce2184c", "gsd-core/workflows/discuss-phase/modes/advisor.md": "30612a2de153cb5b", @@ -350,7 +350,7 @@ "gsd-core/workflows/pr-branch.md": "c67d90c65da47168", "gsd-core/workflows/profile-user.md": "8c943983241260b5", "gsd-core/workflows/progress.md": "65aabee5e8a6dd82", - "gsd-core/workflows/quick.md": "762256cf6d177c06", + "gsd-core/workflows/quick.md": "32a71f62041c8510", "gsd-core/workflows/reapply-patches.md": "ba9406b60f2c4041", "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "433affcd1a200826", diff --git a/tests/fixtures/golden-install-parity/hermes.json b/tests/fixtures/golden-install-parity/hermes.json index f8013837a..dd711e18b 100644 --- a/tests/fixtures/golden-install-parity/hermes.json +++ b/tests/fixtures/golden-install-parity/hermes.json @@ -94,7 +94,7 @@ "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-profiles.md": "8472d453a325cc1b", + "gsd-core/references/model-profiles.md": "6012c3b53473f04f", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -207,13 +207,13 @@ "gsd-core/workflows/autonomous.md": "dd572ca89862e5ec", "gsd-core/workflows/check-todos.md": "ea9a303c48a5d752", "gsd-core/workflows/cleanup.md": "6c488059fc152a49", - "gsd-core/workflows/code-review-fix.md": "04b143a963067236", - "gsd-core/workflows/code-review.md": "ff28724fc216972c", + "gsd-core/workflows/code-review-fix.md": "e829d3baf9901b54", + "gsd-core/workflows/code-review.md": "50a05ab8957bd05f", "gsd-core/workflows/complete-milestone.md": "f1866541148dc291", "gsd-core/workflows/debug.md": "15cf6999e85dcc8c", "gsd-core/workflows/diagnose-issues.md": "16f2d2a85335641f", "gsd-core/workflows/discovery-phase.md": "6161c60d752d0058", - "gsd-core/workflows/discuss-phase-assumptions.md": "db60ec68b59a40eb", + "gsd-core/workflows/discuss-phase-assumptions.md": "3a1e215890d2b3f4", "gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6", "gsd-core/workflows/discuss-phase.md": "0aa052ae4ee70bf6", "gsd-core/workflows/discuss-phase/modes/advisor.md": "536e2fa842f1bc95", @@ -279,7 +279,7 @@ "gsd-core/workflows/pr-branch.md": "ecabd55e4eabf229", "gsd-core/workflows/profile-user.md": "de5030437226cf2c", "gsd-core/workflows/progress.md": "f18db000584d9cb1", - "gsd-core/workflows/quick.md": "5044ce4e7512e174", + "gsd-core/workflows/quick.md": "e7a395b9e7786b56", "gsd-core/workflows/reapply-patches.md": "158083a310859594", "gsd-core/workflows/remove-phase.md": "fce799aae3ab2715", "gsd-core/workflows/remove-workspace.md": "8facde381657dd71", diff --git a/tests/fixtures/golden-install-parity/kilo.json b/tests/fixtures/golden-install-parity/kilo.json index f3596be07..501c826ac 100644 --- a/tests/fixtures/golden-install-parity/kilo.json +++ b/tests/fixtures/golden-install-parity/kilo.json @@ -165,7 +165,7 @@ "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-profiles.md": "d6ed560db6357ad2", + "gsd-core/references/model-profiles.md": "6568ca29b6ee00d8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8", @@ -278,13 +278,13 @@ "gsd-core/workflows/autonomous.md": "73f429f7472c9949", "gsd-core/workflows/check-todos.md": "ed4b4eb12be222d7", "gsd-core/workflows/cleanup.md": "c5f1bf186395d67d", - "gsd-core/workflows/code-review-fix.md": "adb62c695b39ef61", - "gsd-core/workflows/code-review.md": "6c40b95e799907d0", + "gsd-core/workflows/code-review-fix.md": "722416b71b31c5fc", + "gsd-core/workflows/code-review.md": "506412604f767adc", "gsd-core/workflows/complete-milestone.md": "59753bf44d4300da", "gsd-core/workflows/debug.md": "a72d830ac3df74aa", "gsd-core/workflows/diagnose-issues.md": "210b5b313e8a559a", "gsd-core/workflows/discovery-phase.md": "ca7b2be46e59e862", - "gsd-core/workflows/discuss-phase-assumptions.md": "36f7edcb666745f2", + "gsd-core/workflows/discuss-phase-assumptions.md": "3ef1df313e715387", "gsd-core/workflows/discuss-phase-power.md": "5c0d1ca6abda0383", "gsd-core/workflows/discuss-phase.md": "bd4b26ba168bb51f", "gsd-core/workflows/discuss-phase/modes/advisor.md": "f8ae26ba4e07672b", @@ -350,7 +350,7 @@ "gsd-core/workflows/pr-branch.md": "ab157cd8e49621dd", "gsd-core/workflows/profile-user.md": "203ebe3f8f3876a8", "gsd-core/workflows/progress.md": "9381c59676ccb937", - "gsd-core/workflows/quick.md": "fd7461e5a92fe450", + "gsd-core/workflows/quick.md": "e71c92cf0463a461", "gsd-core/workflows/reapply-patches.md": "becf9728cdb124c4", "gsd-core/workflows/remove-phase.md": "ada8a0546c686483", "gsd-core/workflows/remove-workspace.md": "fc83f362a2d0a1b7", diff --git a/tests/fixtures/golden-install-parity/kimi.json b/tests/fixtures/golden-install-parity/kimi.json index 2b3f8fa6b..65d63af8a 100644 --- a/tests/fixtures/golden-install-parity/kimi.json +++ b/tests/fixtures/golden-install-parity/kimi.json @@ -130,7 +130,7 @@ "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-profiles.md": "8484ad9799b7f680", + "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -243,13 +243,13 @@ "gsd-core/workflows/autonomous.md": "7e5683728ef33707", "gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc", "gsd-core/workflows/cleanup.md": "0daa2f2720c111f1", - "gsd-core/workflows/code-review-fix.md": "c323f7098b671bd6", - "gsd-core/workflows/code-review.md": "ad450c6ef8459dad", + "gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075", + "gsd-core/workflows/code-review.md": "334c90c401f291f8", "gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2", "gsd-core/workflows/debug.md": "849d7e5b9c12dcae", "gsd-core/workflows/diagnose-issues.md": "67c058fc7ae6026b", "gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761", - "gsd-core/workflows/discuss-phase-assumptions.md": "1e229f6d21831d60", + "gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88", "gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a", "gsd-core/workflows/discuss-phase.md": "0c0465ba668adb33", "gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653", @@ -315,7 +315,7 @@ "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", "gsd-core/workflows/profile-user.md": "5abfae83739fa978", "gsd-core/workflows/progress.md": "893aa3c36983f74b", - "gsd-core/workflows/quick.md": "e265e01cfe117215", + "gsd-core/workflows/quick.md": "09d88dd1c0b3e03a", "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", diff --git a/tests/fixtures/golden-install-parity/opencode.json b/tests/fixtures/golden-install-parity/opencode.json index ed0c3e395..83a7124d5 100644 --- a/tests/fixtures/golden-install-parity/opencode.json +++ b/tests/fixtures/golden-install-parity/opencode.json @@ -165,7 +165,7 @@ "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-profiles.md": "d6ed560db6357ad2", + "gsd-core/references/model-profiles.md": "6568ca29b6ee00d8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "2459c40bfc8a0ea8", @@ -278,13 +278,13 @@ "gsd-core/workflows/autonomous.md": "6cb0c014f5f56965", "gsd-core/workflows/check-todos.md": "6526b64ee88c7d2e", "gsd-core/workflows/cleanup.md": "94d771d26f62dc86", - "gsd-core/workflows/code-review-fix.md": "5d97b960da4fef73", - "gsd-core/workflows/code-review.md": "aec8ef4de1829607", + "gsd-core/workflows/code-review-fix.md": "adb9388bb157610c", + "gsd-core/workflows/code-review.md": "ae6bcbd1575aeec4", "gsd-core/workflows/complete-milestone.md": "614299b2c08e66c3", "gsd-core/workflows/debug.md": "9d4a8afc8d36be93", "gsd-core/workflows/diagnose-issues.md": "2971c699d52f1b85", "gsd-core/workflows/discovery-phase.md": "724408336596c50c", - "gsd-core/workflows/discuss-phase-assumptions.md": "310b08dc31710cbc", + "gsd-core/workflows/discuss-phase-assumptions.md": "92e43cd12200c610", "gsd-core/workflows/discuss-phase-power.md": "5c0d1ca6abda0383", "gsd-core/workflows/discuss-phase.md": "3a62a8d4c374c69f", "gsd-core/workflows/discuss-phase/modes/advisor.md": "83ca3ea4fac785af", @@ -350,7 +350,7 @@ "gsd-core/workflows/pr-branch.md": "929b7cb0c99c7b9e", "gsd-core/workflows/profile-user.md": "248d59a31948e0ed", "gsd-core/workflows/progress.md": "8fc3404087f50b95", - "gsd-core/workflows/quick.md": "10cc8ddd4bc3f0ee", + "gsd-core/workflows/quick.md": "9bdccb7c6a5530c9", "gsd-core/workflows/reapply-patches.md": "a0e9b53f90abceb2", "gsd-core/workflows/remove-phase.md": "dea4661e8f89596f", "gsd-core/workflows/remove-workspace.md": "446847e71aa52504", diff --git a/tests/fixtures/golden-install-parity/qwen.json b/tests/fixtures/golden-install-parity/qwen.json index 6f24ee22f..71a9ad563 100644 --- a/tests/fixtures/golden-install-parity/qwen.json +++ b/tests/fixtures/golden-install-parity/qwen.json @@ -94,7 +94,7 @@ "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-profiles.md": "cc1efc2942164744", + "gsd-core/references/model-profiles.md": "0b7e06ed2e4abac8", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -207,13 +207,13 @@ "gsd-core/workflows/autonomous.md": "3014ff90115f0daf", "gsd-core/workflows/check-todos.md": "ec8c22920f6b2df1", "gsd-core/workflows/cleanup.md": "6a18b165752e3087", - "gsd-core/workflows/code-review-fix.md": "e14afce87cf0d420", - "gsd-core/workflows/code-review.md": "d6d385b6dadae5a0", + "gsd-core/workflows/code-review-fix.md": "f3725ae9d685bed2", + "gsd-core/workflows/code-review.md": "29125604bed2c467", "gsd-core/workflows/complete-milestone.md": "40085d32b15805c8", "gsd-core/workflows/debug.md": "c23d067580b09f63", "gsd-core/workflows/diagnose-issues.md": "652ae26975f82242", "gsd-core/workflows/discovery-phase.md": "6161c60d752d0058", - "gsd-core/workflows/discuss-phase-assumptions.md": "3865afc23e9cd46a", + "gsd-core/workflows/discuss-phase-assumptions.md": "18712f78bb960ec8", "gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6", "gsd-core/workflows/discuss-phase.md": "8644b72386a51241", "gsd-core/workflows/discuss-phase/modes/advisor.md": "3102430dfad9c012", @@ -279,7 +279,7 @@ "gsd-core/workflows/pr-branch.md": "cef0f65b16d500b4", "gsd-core/workflows/profile-user.md": "263c0693563d98da", "gsd-core/workflows/progress.md": "3b1b2142a74af85c", - "gsd-core/workflows/quick.md": "0a9a7dacc73f8e53", + "gsd-core/workflows/quick.md": "bb9a4a145a7edc36", "gsd-core/workflows/reapply-patches.md": "de0ee8acfe7245b2", "gsd-core/workflows/remove-phase.md": "e8ae4fbbfac700f0", "gsd-core/workflows/remove-workspace.md": "4ac64de862dc650e", diff --git a/tests/fixtures/golden-install-parity/trae.json b/tests/fixtures/golden-install-parity/trae.json index 7c07b71ab..8342ab964 100644 --- a/tests/fixtures/golden-install-parity/trae.json +++ b/tests/fixtures/golden-install-parity/trae.json @@ -94,7 +94,7 @@ "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-profiles.md": "41b0af6f5f77af81", + "gsd-core/references/model-profiles.md": "b4527b0f255d193f", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e562947d1bf5c5a4", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -207,13 +207,13 @@ "gsd-core/workflows/autonomous.md": "c482645c5d1ead46", "gsd-core/workflows/check-todos.md": "0dda8236355e8c9c", "gsd-core/workflows/cleanup.md": "82f65f5214ecd748", - "gsd-core/workflows/code-review-fix.md": "959a65773b325ed2", - "gsd-core/workflows/code-review.md": "b31bbd7dbe5ed303", + "gsd-core/workflows/code-review-fix.md": "f2761f7f8c4a5674", + "gsd-core/workflows/code-review.md": "47663a2922756c5e", "gsd-core/workflows/complete-milestone.md": "6e918b72bd885426", "gsd-core/workflows/debug.md": "197d3642a6704de5", "gsd-core/workflows/diagnose-issues.md": "9274b11a3db98c65", "gsd-core/workflows/discovery-phase.md": "b32b6197b66c9a13", - "gsd-core/workflows/discuss-phase-assumptions.md": "3d05b08eae75c7bd", + "gsd-core/workflows/discuss-phase-assumptions.md": "e376b1cf29379df4", "gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6", "gsd-core/workflows/discuss-phase.md": "4509657816c5566a", "gsd-core/workflows/discuss-phase/modes/advisor.md": "c9cd7db62e76ebcb", @@ -279,7 +279,7 @@ "gsd-core/workflows/pr-branch.md": "79fd55b88ea2db9c", "gsd-core/workflows/profile-user.md": "672821e6b1266645", "gsd-core/workflows/progress.md": "94768f835b0b8908", - "gsd-core/workflows/quick.md": "9bbf907e39688638", + "gsd-core/workflows/quick.md": "a568015cccb53615", "gsd-core/workflows/reapply-patches.md": "21b38c374f19fd78", "gsd-core/workflows/remove-phase.md": "a46c2fe853bf4e86", "gsd-core/workflows/remove-workspace.md": "ae0e1c6d4438d663", diff --git a/tests/fixtures/golden-install-parity/windsurf.json b/tests/fixtures/golden-install-parity/windsurf.json index 4993ab62d..a2d03a845 100644 --- a/tests/fixtures/golden-install-parity/windsurf.json +++ b/tests/fixtures/golden-install-parity/windsurf.json @@ -94,7 +94,7 @@ "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-profiles.md": "b3ac5dc094bce1dc", + "gsd-core/references/model-profiles.md": "09aa53e3f1764a41", "gsd-core/references/mvp-concepts.md": "3464783eaaef5c10", "gsd-core/references/phase-argument-parsing.md": "e562947d1bf5c5a4", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -207,13 +207,13 @@ "gsd-core/workflows/autonomous.md": "fe7bb7c978f305a2", "gsd-core/workflows/check-todos.md": "afc840fceb07bf99", "gsd-core/workflows/cleanup.md": "93c14107979a4428", - "gsd-core/workflows/code-review-fix.md": "9b2de93a6087774d", - "gsd-core/workflows/code-review.md": "2f0ebfb7a2e33764", + "gsd-core/workflows/code-review-fix.md": "b99b1f20bb27c291", + "gsd-core/workflows/code-review.md": "faa87faf07ae765a", "gsd-core/workflows/complete-milestone.md": "f463bf4e86ac26f6", "gsd-core/workflows/debug.md": "52243eb936a43150", "gsd-core/workflows/diagnose-issues.md": "447072aa72385271", "gsd-core/workflows/discovery-phase.md": "7dcf150998559c11", - "gsd-core/workflows/discuss-phase-assumptions.md": "f57000d5c5fb178a", + "gsd-core/workflows/discuss-phase-assumptions.md": "b091b3d3e580dd29", "gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6", "gsd-core/workflows/discuss-phase.md": "ce4e6abce9cb2d4e", "gsd-core/workflows/discuss-phase/modes/advisor.md": "cb49f485c4a1f09e", @@ -279,7 +279,7 @@ "gsd-core/workflows/pr-branch.md": "acd59f915d018ad4", "gsd-core/workflows/profile-user.md": "c4313672b81b5bcd", "gsd-core/workflows/progress.md": "18813a345bd2343a", - "gsd-core/workflows/quick.md": "3acd391cc0c3813a", + "gsd-core/workflows/quick.md": "b7813e1810c683e1", "gsd-core/workflows/reapply-patches.md": "d449a23d3acf6379", "gsd-core/workflows/remove-phase.md": "e7a6af429b36e77b", "gsd-core/workflows/remove-workspace.md": "b5e60fbb33b3e33a", diff --git a/tests/fixtures/golden-install-parity/zcode.json b/tests/fixtures/golden-install-parity/zcode.json index 5c044e9eb..ed40c52c3 100644 --- a/tests/fixtures/golden-install-parity/zcode.json +++ b/tests/fixtures/golden-install-parity/zcode.json @@ -165,7 +165,7 @@ "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-profiles.md": "8484ad9799b7f680", + "gsd-core/references/model-profiles.md": "e067ad3df6770db1", "gsd-core/references/mvp-concepts.md": "72f7e8b1f8ae7118", "gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34", "gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac", @@ -278,13 +278,13 @@ "gsd-core/workflows/autonomous.md": "7e5683728ef33707", "gsd-core/workflows/check-todos.md": "bdeaf43f9c61e0cc", "gsd-core/workflows/cleanup.md": "0daa2f2720c111f1", - "gsd-core/workflows/code-review-fix.md": "c323f7098b671bd6", - "gsd-core/workflows/code-review.md": "ad450c6ef8459dad", + "gsd-core/workflows/code-review-fix.md": "2e113d1f4350a075", + "gsd-core/workflows/code-review.md": "334c90c401f291f8", "gsd-core/workflows/complete-milestone.md": "c1f91b77f4ace7f2", "gsd-core/workflows/debug.md": "849d7e5b9c12dcae", "gsd-core/workflows/diagnose-issues.md": "aa8d787db8f3c46c", "gsd-core/workflows/discovery-phase.md": "3ba7cfb89fb1e761", - "gsd-core/workflows/discuss-phase-assumptions.md": "1e229f6d21831d60", + "gsd-core/workflows/discuss-phase-assumptions.md": "f5b765d33eba4f88", "gsd-core/workflows/discuss-phase-power.md": "0841f7dc6e9a054a", "gsd-core/workflows/discuss-phase.md": "d0a67b6f77b4d339", "gsd-core/workflows/discuss-phase/modes/advisor.md": "db488d74d080c653", @@ -350,7 +350,7 @@ "gsd-core/workflows/pr-branch.md": "c87db7ac8c28be1b", "gsd-core/workflows/profile-user.md": "e23bea0a69c0bb4b", "gsd-core/workflows/progress.md": "893aa3c36983f74b", - "gsd-core/workflows/quick.md": "5747c2d8a37affbb", + "gsd-core/workflows/quick.md": "12917fdc9a624a0c", "gsd-core/workflows/reapply-patches.md": "39050f72601aec89", "gsd-core/workflows/remove-phase.md": "df9a45f0b1880999", "gsd-core/workflows/remove-workspace.md": "a7ca66db6b7c132c", diff --git a/tests/loop-render-hooks.test.cjs b/tests/loop-render-hooks.test.cjs index dc82b982a..ef91f7313 100644 --- a/tests/loop-render-hooks.test.cjs +++ b/tests/loop-render-hooks.test.cjs @@ -1039,79 +1039,136 @@ describe('Phase 4 regression: capabilityStatesById gates on active (not enabled) }); }); -// ─── ADR-1244 D2 fail-closed gate injection ──────────────────────────────────── +// ─── ADR-1244 D2: load-failed capability gates FAIL OPEN with a loud warning (#2009) ── -describe('ADR-1244 D2: fail-closed gate injection for skipped overlay caps with gates', () => { - // Verifies that cmdLoopRenderHooks injects a BLOCKING synthetic gate at the - // declared point when an overlay capability that declares a gate is skipped at - // load time due to an incompatible engines.gsd version constraint. - // - // Fixture: overlay cap declares a gate at execute:wave:post with engines.gsd: ">=99.0.0" - // → loadRegistry skips it → records it in _overlay.blockedGates - // → cmdLoopRenderHooks injects a blocking=true, onError=halt gate at execute:wave:post +describe('ADR-1244 D2: load-failed capability gates fail OPEN with a loud warning (#2009)', () => { + // Decision (#2009): a capability that fails to LOAD must not block the loop. + // cmdLoopRenderHooks injects NO gate (the loop proceeds — fail open) and emits a + // loud warning naming the load reason and the exact `gsd capability remove ` + // remediation, both to STDERR (the channel host workflows actually surface) and + // in the envelope's `warnings` array. Previously it injected a blocking=true, + // onError=halt gate that halted every declared point project-wide. - test('skipped gate-kind overlay cap → BLOCKING synthetic gate at its declared point', (t) => { - const overlayHome = fs.mkdtempSync(path.join(os.tmpdir(), 'loop-fail-closed-')); - t.after(() => cleanup(overlayHome)); - - // Write an overlay capability that: - // - declares a gate at execute:wave:post - // - has engines.gsd: ">=99.0.0" (incompatible → will be skipped at load) - const capId = 'fail-closed-gate-cap'; + // Helper: write an overlay cap with an incompatible engines.gsd (→ skipped at load) + // that declares gate(s) at the given point(s). `capId` may be an invalid id to + // exercise the sanitization path. + function writeSkippedGateCap(overlayHome, capId, gatePoints) { const capDir = path.join(overlayHome, '.gsd', 'capabilities', capId); fs.mkdirSync(capDir, { recursive: true }); const capManifest = { id: capId, role: 'feature', version: '1.0.0', - title: 'Fail Closed Gate Cap', - description: 'ADR-1244 D2 fail-closed wiring test', + title: 'Load-Failed Gate Cap', + description: 'ADR-1244 D2 fail-open wiring test', tier: 'standard', requires: [], - engines: { gsd: '>=99.0.0' }, // intentionally incompatible → always skipped + engines: { gsd: '>=99.0.0' }, // intentionally incompatible → always skipped at load runtimeCompat: { supported: ['*'], unsupported: [] }, skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [], - gates: [{ point: 'execute:wave:post', check: 'always-pass', blocking: true, onError: 'halt' }], + gates: gatePoints.map((point) => ({ point, check: { query: 'always-pass' }, blocking: true, onError: 'halt' })), }; fs.writeFileSync(path.join(capDir, 'capability.json'), JSON.stringify(capManifest), 'utf8'); + } - // Invoke gsd-tools via subprocess so stdout is the real fd-1 (io.cjs writes via writeSync). - // Set GSD_HOME to the overlay home so loadRegistry picks up the incompatible cap. + function renderHooks(overlayHome, point, extraArgs = []) { const result = spawnSync( process.execPath, - [GSD_TOOLS, 'loop', 'render-hooks', 'execute:wave:post', '--cwd', overlayHome], - { - cwd: ROOT, - encoding: 'utf8', - env: { ...process.env, GSD_HOME: overlayHome }, - }, + [GSD_TOOLS, 'loop', 'render-hooks', point, '--cwd', overlayHome, ...extraArgs], + { cwd: ROOT, encoding: 'utf8', env: { ...process.env, GSD_HOME: overlayHome } }, ); + assert.strictEqual(result.status, 0, `Expected exit 0 at ${point}. stderr: ` + (result.stderr || '')); + return result; + } - assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || '')); - - let envelope; + function parseEnvelope(result) { try { - envelope = JSON.parse(result.stdout.trim()); + return JSON.parse(result.stdout.trim()); } catch { assert.fail('loop render-hooks output must be valid JSON; got: ' + result.stdout.slice(0, 300)); } + } - // The synthetic blocking gate must be present in activeHooks - const syntheticGate = Array.isArray(envelope.activeHooks) - ? envelope.activeHooks.find((h) => h.capId === capId && h.kind === 'gate') + test('load-failed gate-kind overlay cap → NO gate injected (fail open) + loud warning on stderr and in envelope', (t) => { + const overlayHome = fs.mkdtempSync(path.join(os.tmpdir(), 'loop-fail-open-')); + t.after(() => cleanup(overlayHome)); + + const capId = 'load-failed-gate-cap'; + writeSkippedGateCap(overlayHome, capId, ['execute:wave:post']); + + const result = renderHooks(overlayHome, 'execute:wave:post'); + const envelope = parseEnvelope(result); + + // AC2 / AC-a: fail OPEN — NO gate (blocking or otherwise) is injected for the + // load-failed cap, so the loop proceeds. + const anyGate = Array.isArray(envelope.activeHooks) + ? envelope.activeHooks.find((h) => h.capId === capId) : undefined; - assert.ok( - syntheticGate !== undefined, - `activeHooks must contain a synthetic gate attributed to ${capId} (fail-closed injection). ` + - 'Got: ' + JSON.stringify(envelope.activeHooks), + assert.strictEqual( + anyGate, undefined, + 'no hook must be injected for a load-failed cap (fail open). Got: ' + JSON.stringify(envelope.activeHooks), ); - assert.strictEqual(syntheticGate.blocking, true, 'synthetic gate must be blocking=true'); - assert.strictEqual(syntheticGate.onError, 'halt', 'synthetic gate must have onError=halt'); - // The rendered markdown must also reference the gate cap + // AC-b: a loud warning is surfaced in the envelope `warnings` channel... + const warnEnv = (envelope.warnings || []).find((w) => w.includes(capId)); + assert.ok(warnEnv, 'envelope.warnings must name the load-failed cap. Got: ' + JSON.stringify(envelope.warnings)); + // AC1: ...carrying the exact remediation, and making clear the gate is not enforced. assert.ok( - typeof envelope.rendered === 'string' && envelope.rendered.includes(capId), - 'rendered output must reference the fail-closed gate cap. Got: ' + envelope.rendered, + warnEnv.includes(`gsd capability remove ${capId}`), + 'warning must include the `gsd capability remove ` remediation. Got: ' + warnEnv, + ); + assert.match(warnEnv, /skipped|not enforced|failing open/i, 'warning must say the gate is not enforced. Got: ' + warnEnv); + + // ...and ALSO to stderr (the channel host workflows actually see). + assert.ok( + result.stderr.includes(`gsd capability remove ${capId}`), + 'stderr must carry the loud fail-open warning with remediation. Got: ' + result.stderr, + ); + }); + + test('load-failed cap declaring gates at ship:pre AND verify:post → neither point blocks (project can ship & verify)', (t) => { + const overlayHome = fs.mkdtempSync(path.join(os.tmpdir(), 'loop-fail-open-2pt-')); + t.after(() => cleanup(overlayHome)); + + const capId = 'load-failed-two-point-cap'; + writeSkippedGateCap(overlayHome, capId, ['ship:pre', 'verify:post']); + + for (const point of ['ship:pre', 'verify:post']) { + const result = renderHooks(overlayHome, point); + const envelope = parseEnvelope(result); + const injected = envelope.activeHooks.find((h) => h.capId === capId); + assert.strictEqual( + injected, undefined, + `${point} must NOT inject any hook for a load-failed cap (fail open). Got: ` + JSON.stringify(envelope.activeHooks), + ); + const warn = (envelope.warnings || []).find((w) => w.includes(`gsd capability remove ${capId}`)); + assert.ok(warn, `${point} must surface a fail-open warning with remediation. Got: ` + JSON.stringify(envelope.warnings)); + } + }); + + test('security: an invalid (non-kebab) capability id is withheld — no runnable remove command is rendered (#2009 review)', (t) => { + const overlayHome = fs.mkdtempSync(path.join(os.tmpdir(), 'loop-fail-open-evil-')); + t.after(() => cleanup(overlayHome)); + + // A directory name that is a valid POSIX filename but NOT a valid capability + // id, and contains a shell metacharacter. It must never appear inside a + // runnable `gsd capability remove ` command in the surfaced warning. + const evilId = 'Bad;Cap'; + writeSkippedGateCap(overlayHome, evilId, ['ship:pre']); + + const result = renderHooks(overlayHome, 'ship:pre'); + const envelope = parseEnvelope(result); + + const combined = (envelope.warnings || []).join('\n') + '\n' + result.stderr; + // The raw metacharacter id must not be embedded in a runnable remove command. + assert.ok( + !combined.includes(`gsd capability remove ${evilId}`), + 'invalid capability id must NOT be placed in a runnable remove command. Got: ' + combined, + ); + // A warning is still surfaced (fail-open is loud), using the withheld-id form. + assert.match( + combined, /invalid id \(withheld\)/, + 'an invalid id must be reported via the withheld-id placeholder. Got: ' + combined, ); }); }); diff --git a/tests/model-resolver.test.cjs b/tests/model-resolver.test.cjs index 3e2d06ddf..b902d5149 100644 --- a/tests/model-resolver.test.cjs +++ b/tests/model-resolver.test.cjs @@ -120,6 +120,36 @@ describe('resolveModelInternal', () => { assert.ok(typeof model === 'string' && model.length > 0); }); + // #2072 acceptance: these two catalog agents' config MUST resolve — the bug was + // that the workflows never threaded the resolved value, not that the resolver + // ignored it. These assert the value the (now-threaded) spawns receive. + test('#2072: model_overrides applies to gsd-code-reviewer', () => { + writeConfig(tmpDir, { model_overrides: { 'gsd-code-reviewer': 'my-custom-model' } }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-code-reviewer'), 'my-custom-model'); + }); + + test('#2072: model_overrides applies to gsd-assumptions-analyzer', () => { + writeConfig(tmpDir, { model_overrides: { 'gsd-assumptions-analyzer': 'my-custom-model' } }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-assumptions-analyzer'), 'my-custom-model'); + }); + + test('#2072: models.verification tier applies to gsd-code-reviewer', () => { + writeConfig(tmpDir, { models: { verification: 'opus' } }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-code-reviewer'), 'opus'); + }); + + test('#2072: models.discuss tier applies to gsd-assumptions-analyzer', () => { + writeConfig(tmpDir, { models: { discuss: 'opus' } }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-assumptions-analyzer'), 'opus'); + }); + + test('#2072: model_overrides + models.execution apply to gsd-code-fixer', () => { + writeConfig(tmpDir, { model_overrides: { 'gsd-code-fixer': 'my-custom-model' } }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-code-fixer'), 'my-custom-model'); + writeConfig(tmpDir, { models: { execution: 'opus' } }); + assert.strictEqual(resolveModelInternal(tmpDir, 'gsd-code-fixer'), 'opus'); + }); + test('runtime non-claude + model_profile_overrides for runtime tier', () => { writeConfig(tmpDir, { runtime: 'codex', diff --git a/tests/model-routing-spawn-threading.test.cjs b/tests/model-routing-spawn-threading.test.cjs new file mode 100644 index 000000000..bfa6a2ebc --- /dev/null +++ b/tests/model-routing-spawn-threading.test.cjs @@ -0,0 +1,150 @@ +// allow-test-rule: source-text-is-the-product #2072 +// Workflow .md files ARE the deployed orchestration contract the runtime executes; +// asserting that a spawn threads a resolved model= is asserting the deployed contract. + +'use strict'; + +/** + * #2072 — model_overrides / models. were silently inert for + * gsd-assumptions-analyzer and gsd-code-reviewer on Claude: the resolver honored + * them, but the workflows spawned the agents with NO model= param, so the resolved + * value never reached the Agent tool and the agents inherited the session model. + * + * Fix contract: every spawn of these two catalog agents must thread a resolved + * model=, and the workflow must obtain that model (inline `resolve-model` for the + * single-agent workflows, or the `reviewer_model` field of the init.quick bundle). + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const WF = path.join(__dirname, '..', 'gsd-core', 'workflows'); +const read = (rel) => fs.readFileSync(path.join(WF, rel), 'utf-8'); + +// Every .md under gsd-core/workflows (incl. nested steps/ and modes/). +function allWorkflowMd() { + const out = []; + (function walk(dir, rel) { + for (const e of fs.readdirSync(dir, { withFileTypes: true })) { + if (e.isDirectory()) walk(path.join(dir, e.name), path.join(rel, e.name)); + else if (e.name.endsWith('.md')) out.push(path.join(rel, e.name)); + } + })(WF, ''); + return out; +} + +// Return the full text of every `Agent( … )` call in `content`, tracking string +// state (triple- and single-double-quoted) and paren depth so a prompt body's own +// parens/quotes don't end the call early. Order-independent: a call's params are +// captured whether subagent_type= appears before or after the prompt. +function agentCalls(content) { + const calls = []; + const re = /Agent\(/g; + let m; + while ((m = re.exec(content)) !== null) { + let i = m.index + m[0].length; + let depth = 1; + let tq = false; // inside """ … """ + let sq = false; // inside " … " + while (i < content.length && depth > 0) { + if (tq) { + if (content.startsWith('"""', i)) { tq = false; i += 3; continue; } + i++; continue; + } + if (sq) { + if (content[i] === '\\') { i += 2; continue; } + if (content[i] === '"') { sq = false; } + i++; continue; + } + if (content.startsWith('"""', i)) { tq = true; i += 3; continue; } + if (content[i] === '"') { sq = true; i++; continue; } + if (content[i] === '(') { depth++; } + else if (content[i] === ')') { depth--; } + i++; + } + calls.push(content.slice(m.index, i)); + re.lastIndex = i; // don't re-scan inside this call + } + return calls; +} + +describe('#2072: routed-agent spawns thread the resolved model', () => { + test('discuss-phase-assumptions.md resolves + threads gsd-assumptions-analyzer model', () => { + const c = read('discuss-phase-assumptions.md'); + assert.match(c, /resolve-model gsd-assumptions-analyzer/, 'must resolve the routed model'); + assert.match( + c, + /subagent_type="gsd-assumptions-analyzer",\s*model="\{ANALYZER_MODEL\}"/, + 'spawn must thread the resolved model=', + ); + }); + + test('code-review.md resolves + threads gsd-code-reviewer model', () => { + const c = read('code-review.md'); + assert.match(c, /resolve-model gsd-code-reviewer/); + assert.match(c, /subagent_type="gsd-code-reviewer",\s*model="\{REVIEWER_MODEL\}"/); + }); + + test('code-review-fix.md re-review resolves + threads gsd-code-reviewer model', () => { + const c = read('code-review-fix.md'); + assert.match(c, /resolve-model gsd-code-reviewer/); + assert.match(c, /subagent_type="gsd-code-reviewer",\s*model="\{REVIEWER_MODEL\}"/); + }); + + test('quick.md review step threads gsd-code-reviewer own model (not executor_model)', () => { + const c = read('quick.md'); + // reviewer_model comes from the init.quick bundle; the spawn must use it. + assert.match(c, /subagent_type="gsd-code-reviewer",\s*\n\s*model="\{reviewer_model\}"/); + assert.doesNotMatch( + c, + /subagent_type="gsd-code-reviewer",\s*\n\s*model="\{executor_model\}"/, + 'reviewer must not reuse the executor model (own model_overrides would be ignored)', + ); + }); + + test('code-review-fix.md threads gsd-code-fixer model at both fixer spawns', () => { + const c = read('code-review-fix.md'); + assert.match(c, /resolve-model gsd-code-fixer/, 'must resolve the fixer model'); + const fixerSpawns = c.match(/subagent_type="gsd-code-fixer", model="\{FIXER_MODEL\}"/g) || []; + assert.strictEqual(fixerSpawns.length, 2, 'both gsd-code-fixer spawns must thread FIXER_MODEL'); + }); + + // Parity guard: EVERY spawn of the fixed routed agents across ALL workflows must + // carry a model= in its Agent(...) call, so a new silently-inert spawn cannot + // regress. Uses a quote/paren-aware scan of the WHOLE Agent(...) call, so it holds + // for single-line and multi-line calls, multiple spawns per file, and either param + // ordering (subagent_type= before OR after the prompt body). + test('no spawn of the fixed routed agents is missing a model= (parity guard)', () => { + const ROUTED = /subagent_type="(gsd-code-reviewer|gsd-assumptions-analyzer|gsd-code-fixer)"/; + const offenders = []; + for (const rel of allWorkflowMd()) { + for (const call of agentCalls(read(rel))) { + const routed = call.match(ROUTED); + if (routed && !/\bmodel\s*=/.test(call)) { + offenders.push(`${rel}: ${routed[1]} spawn missing model=`); + } + } + } + assert.deepEqual(offenders, [], `routed-agent spawn(s) missing model=:\n${offenders.join('\n')}`); + }); + + // The scanner itself must catch a prompt-first, un-threaded spawn (the exact blind + // spot a naive "params before prompt=" heuristic misses) — otherwise the guard + // above could pass vacuously. + test('parity guard detects a prompt-first spawn that omits model=', () => { + const synthetic = [ + 'Agent(', + ' prompt="""do the thing (with parens) and a " quote""",', + ' subagent_type="gsd-code-reviewer"', + ')', + ].join('\n'); + const [call] = agentCalls(synthetic); + assert.ok(/subagent_type="gsd-code-reviewer"/.test(call), 'scanner must capture the prompt-first subagent_type'); + assert.ok(!/\bmodel\s*=/.test(call), 'and correctly see that model= is absent'); + // A well-formed prompt-first spawn WITH model= must be accepted. + const ok = agentCalls(synthetic.replace(')', ' model="{reviewer_model}"\n)'))[0]; + assert.ok(/\bmodel\s*=/.test(ok)); + }); +}); diff --git a/tests/perf-316-state-lock-buffer-alloc.test.cjs b/tests/perf-316-state-lock-buffer-alloc.test.cjs index 3924cc5ef..4e0e04328 100644 --- a/tests/perf-316-state-lock-buffer-alloc.test.cjs +++ b/tests/perf-316-state-lock-buffer-alloc.test.cjs @@ -57,9 +57,13 @@ const fs = require('fs'); // Write pid to lock file so acquireStateLock sees a live pid and retries. fs.writeFileSync(workerData.lockPath, String(process.pid)); parentPort.postMessage({ pid: process.pid }); -// Synchronous sleep — blocks this worker thread for holdMs ms. -const buf = new Int32Array(new SharedArrayBuffer(4)); -Atomics.wait(buf, 0, 0, workerData.holdMs); +// Hold the lock until the writer signals it has made its first FAILED lock +// attempt (deterministic contention), or until holdMs elapses as a safety cap. +// Atomics.wait blocks this thread while releaseFlag[0] === 0. A fixed timer here +// was racy: on a slow/loaded runner the writer's spawn+init could exceed the +// timer, so the lock released before the writer ever contended (lockAttempts:1). +const releaseFlag = new Int32Array(workerData.releaseSab); +Atomics.wait(releaseFlag, 0, 0, workerData.holdMs); // Release the lock. try { fs.unlinkSync(workerData.lockPath); } catch { /* already gone */ } parentPort.postMessage({ done: true }); @@ -88,17 +92,29 @@ global.SharedArrayBuffer = StubSAB; // path — without this witness, a no-retry success would yield sabCount === 1 // from BOTH pre-fix and post-fix code (the SAB is allocated unconditionally // post-fix, and exactly once for the single successful open pre-fix), giving -// a false-pass against the bug. The 1000ms holdMs + 200ms SUT retry delay -// guarantees >=4 attempts even on the slowest CI runners. +// a false-pass against the bug. Contention is deterministic here: this worker +// signals the holder to release only after its first failed attempt, so a +// retry always occurs (lockAttempts >= 2) regardless of runner speed. const realOpenSync = fs.openSync.bind(fs); +const releaseFlag = new Int32Array(workerData.releaseSab); let lockAttempts = 0; fs.openSync = function(filePath, flags, mode) { - if (typeof filePath === 'string' && filePath.endsWith('.lock') && + const isLockCreate = typeof filePath === 'string' && filePath.endsWith('.lock') && typeof flags === 'number' && - (flags & fs.constants.O_CREAT) && (flags & fs.constants.O_EXCL)) { - lockAttempts++; + (flags & fs.constants.O_CREAT) && (flags & fs.constants.O_EXCL); + if (isLockCreate) lockAttempts++; + try { + return realOpenSync(filePath, flags, mode); + } catch (e) { + // On the FIRST failed atomic-create (lock is held → contention proven), + // signal the holder worker to release so the next retry succeeds. Makes the + // retry path deterministic regardless of worker-spawn latency. + if (isLockCreate && lockAttempts === 1) { + Atomics.store(releaseFlag, 0, 1); + Atomics.notify(releaseFlag, 0); + } + throw e; } - return realOpenSync(filePath, flags, mode); }; // Delete cache entry to ensure a fresh require picks up the stubbed constructor. @@ -160,17 +176,18 @@ describe('perf #316: acquireStateLock hoists sleep buffer — exactly one SAB pe { timeout: 8000 }, async () => { // ── Worker A: hold the lock for 1000ms ───────────────────────────────── - // state.cjs retry delay = 200ms + 0-50ms jitter; 1000ms hold guarantees - // >=4 retries even on the slowest CI worker (~200ms spawn + 4 retry - // intervals ~1000ms ≈ hold duration). The lockAttempts assertion below - // proves the retry path was exercised end-to-end. + // The holder releases the lock only when the writer signals its first + // failed lock attempt (see WRITER_WORKER_CODE), so the writer is guaranteed + // to contend at least once regardless of worker-spawn latency. holdMs is now + // only a safety cap in case that signal never arrives. const holdMs = 1000; + const releaseSab = new SharedArrayBuffer(4); let resolveLockWritten; const lockWritten = new Promise((resolve) => { resolveLockWritten = resolve; }); const holderDone = new Promise((resolve, reject) => { holderWorker = new Worker(HOLDER_WORKER_CODE, { eval: true, - workerData: { lockPath, holdMs }, + workerData: { lockPath, holdMs, releaseSab }, }); holderWorker.on('message', (msg) => { if (msg.pid !== undefined) resolveLockWritten(); @@ -223,6 +240,7 @@ describe('perf #316: acquireStateLock hoists sleep buffer — exactly one SAB pe statePath, content: MINIMAL_STATE_MD, tmpDir, + releaseSab, }, }); writerWorker.on('message', resolve); @@ -259,8 +277,8 @@ describe('perf #316: acquireStateLock hoists sleep buffer — exactly one SAB pe assert.ok( writeResult.lockAttempts >= 2, 'SUT must have entered the retry path (>=1 failed lock attempt before success). ' + - 'Got lockAttempts: ' + writeResult.lockAttempts + '. The 1000ms holdMs + 200ms ' + - 'SUT retry delay guarantees >=2 attempts on any CI runner.' + 'Got lockAttempts: ' + writeResult.lockAttempts + '. The holder releases only ' + + 'after the writer signals its first failed attempt, so contention is deterministic.' ); // THE KEY INVARIANT: diff --git a/tests/workflow-size-baseline.json b/tests/workflow-size-baseline.json index d389e7147..2f8dfb639 100644 --- a/tests/workflow-size-baseline.json +++ b/tests/workflow-size-baseline.json @@ -11,13 +11,13 @@ "autonomous.md": 42474, "check-todos.md": 9475, "cleanup.md": 9941, - "code-review-fix.md": 23934, - "code-review.md": 31646, + "code-review-fix.md": 24320, + "code-review.md": 31916, "complete-milestone.md": 31071, "debug.md": 13549, "diagnose-issues.md": 12864, "discovery-phase.md": 8651, - "discuss-phase-assumptions.md": 27028, + "discuss-phase-assumptions.md": 27302, "discuss-phase-power.md": 11273, "discuss-phase.md": 31986, "do.md": 10353, @@ -59,7 +59,7 @@ "pr-branch.md": 15963, "profile-user.md": 21246, "progress.md": 30599, - "quick.md": 50452, + "quick.md": 50470, "reapply-patches.md": 20312, "remove-phase.md": 8513, "remove-workspace.md": 7551,