From ed360cd99f897e834bf005384865a68290297a47 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 4 Aug 2026 18:10:31 -0400 Subject: [PATCH] chore(#2995): extend fragment emission to agents/ and reclaim size-cap headroom (#3058) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(#2995): extend fragment emission to agents/ across every read point Epic #1671 Phase 6.4. `composeWorkflow` stripped `` markers only for `gsd-core/workflows/`, so a marked agent shipped its markers verbatim into every runtime — and agent text is loaded into a subagent's context on every dispatch. The issue proposed widening the `copyWithPathReplacement` guard. That is a no-op for agents: agents never traverse that function. Agent content is read for emission at five independent points, and the obvious chokepoint `stageAgentsForProfile` short-circuits on the DEFAULT `full` profile (`skills === '*'` returns the real unstaged directory), so a hook placed there is dead code on most installs. Composition now happens at two call sites instead of five parallel surfaces: `stageAgentsForRuntimeWithConverter` (with `agentsKind` and `kimiAgentsKind` routed through it via an identity converter) and the inline agent loop in bin/install.js. Both compose BEFORE any path rewrite, so a `.claude/` -> `.windsurf/` regex can never reach inside a marker attribute — the ordering #2930 established for workflows. `installCodexConfig` was the fifth read point: Codex embeds each agent's prompt into a per-agent `.toml` via its own readFileSync. Call-graph analysis missed it; the exhaustive per-runtime emission sweep found it. That is why the new guard is behavioral rather than structural — a sixth read point fails the sweep without anyone remembering to extend a list. tests/agent-fragments-emission.install.test.cjs spawns a real installer for every runtime at every agent-bearing scope, derived from RUNTIME_META and the capability registry at run time so a new runtime cannot be silently under-covered. It asserts markers are absent AND the `when="always"` body is retained, so marker-absence cannot be satisfied by dropping content. An identity-composer negative control proves the assertion can fail. Verified: 0 install failures, 0 marker leaks, body retained on 27 runtime/scope paths; red before the wiring on claude(global+local), zcode(global+local), kimi, codex and opencode. Refs #2995 * chore(#2995): give the tightest agents headroom and correct the design lock Epic #1671 Phase 6.4, second half. `agents/gsd-verifier.md` had 12 bytes of headroom under its 49,152-byte LARGE cap and `agents/gsd-debugger.md` had 147 under its 57,344-byte XL cap. Both now extract reference material to `gsd-core/references/` behind an @-reference — the documented DEFECT.AGENT-FILE-SIZE-CAP-BREACH remedy: gsd-verifier 49,140 -> 46,371 B headroom 12 -> 2,781 gsd-debugger 57,197 -> 48,851 B headroom 147 -> 8,493 Byte accounting proves no content was lost: the combined agent+reference delta is exactly the new files' headers plus the agents' slim replacement blocks. Each agent keeps its routing table and a one-line summary per entry, so it degrades gracefully on a runtime that does not inline @-references. `agents/gsd-planner.md` is untouched and still passes both char guards (49,130 < 49,152); it needed no change, so it took none. The other nine LARGE/XL agents carry NO gsd:section markers, and that is deliberate, not deferred. `when=` selection is read from gsd-core/workflows/section-manifest.json, which gen-section-manifest.cjs derives from gsd-core/workflows/*.md only — shape `{workflows: ...}`, no per-agent key, no per-agent init entry point. An agent atom therefore fails admission gate (2) ("a fact the init seam demonstrably computes at a real entry point") and would evaluate false forever while looking like working gating. Marking agents would manufacture exactly the silent-inertness rot the frozen vocabulary exists to prevent. ADR-1671 gains three amendments, two of which close gaps /adr-phase-coverage found against what actually merged: - The 19 -> 29 vocabulary widening shipped in #2994 with no coordinated ADR amendment, which that bullet's own rule forbids. Recorded now. - `flag:--verify-only` was one of six atoms #2992 withheld and deferred to "the LARGE/XL rollout phase". Five shipped; this one is permanently rejected, and that disposition lived only in a merged PR body. - Phase 6.4's own finding: emission extends to agents/, gating does not. CONTEXT.md's glossary was stale on both seams — Workflow Fragments Module still listed the original 4-atom vocabulary and described when= as "not yet acted on", and Section Manifest Module still described InvocationFacts as {waveFlag, phaseNumber, hasPriorPhases}. Both now match the shipped contract. Inventory manifest regenerated AFTER build:lib per the documented ordering landmine; 19 install-tree fixtures pick up the two new references. Refs #2995 * chore(#2995): correct the compose-site count and mark the raw stager Self-review found two comment defects in the prior commit. The agentsKind comment claimed composition lands at TWO call sites; it is three, since installCodexConfig's per-agent .toml writer was added after that comment was written. And stageAgentsForProfile is now production-dead — both callers route through the composing stager — while staying exported and unit-tested, which makes it a trap: it does a raw copyFileSync and short-circuits to the unstaged source directory under the default profile, so a future caller would silently reintroduce the marker-shipping path. Its JSDoc now says so. * test(#2995): guard the marker-documenting-doc class for agents Widening the composer's scope to agents/ makes reachable the exact class #2930 narrowed scope to avoid: a file that DOCUMENTS the marker syntax with an unfenced example is indistinguishable from a real marker, so the composer drops that line from the emitted artifact. Three rows. A fenced example must compose byte-identically. No shipped agent may carry a marker outside a fence — asserted by parsing every real agent and requiring zero explicit sections, which is what makes the fence protection load-bearing rather than decorative. And a non-vacuity row asserts an UNFENCED marker IS parsed as a real marker, so if that ever stops being true the second row is guarding nothing. Also applies two review findings: stageAgentsForProfile's new JSDoc claimed it had no production caller, which is false — bin/install.js's _stageAgents still calls it, and its consumers compose before writing. Corrected to state the invariant instead. And a let/const nit in the emission sweep. * fix(#2995): keep verifier status vocabulary in the agent, fix a wrong fixture The first remote run came back red with three failures. Both root causes were mine. 1. tests/agent-frontmatter.test.cjs requires agents/gsd-verifier.md to literally contain HOLLOW and DISCONNECTED. The Step 4b extraction moved that status vocabulary into gsd-core/references/verifier-wiring-patterns.md, so the agent no longer had it. Byte accounting said no content was lost, and byte-wise that was true — but a contract required those tokens to live IN THE AGENT. That is ADR-1671:66's flexReserve floor stated concretely: a load-bearing fragment must not be trimmed out of its host, and "the bytes still exist somewhere" is not the test. The two status tables are restored to the agent and deliberately mirrored in the reference with a note saying so, so the procedure there still reads standalone. gsd-verifier lands at 47,069 B — headroom 12 -> 2,083, rather than the 2,781 the first attempt claimed. 2. Row 12b of the new marker-documentation guard asserted that an unfenced marker example parses as a real marker, and threw instead: "unmatched /gsd:section close marker". The grammar is WHOLE-LINE only. The fixture had put the OPEN marker inline mid-sentence, so it was correctly not recognised as an open while the close, on its own line, was. That is a real refinement of the hazard this guard exists for: only a marker on its OWN line is mis-parsed — which is exactly how a documentation example is normally written. Row 12b now uses a whole-line marker, and a new row 12c pins the inline case as explicitly NOT a marker. No test was weakened to accommodate the change; the change was corrected to satisfy the tests. Refs #2995 * chore(#2995): backfill changeset pr number to 3058 --------- Co-authored-by: sim --- .changeset/agile-pumas-gather.md | 5 + CONTEXT.md | 4 +- agents/gsd-debugger.md | 258 +----------------- agents/gsd-verifier.md | 84 +----- bin/install.js | 18 +- docs/INVENTORY-MANIFEST.json | 2 + docs/INVENTORY.md | 2 + ...671-dynamic-context-management-platform.md | 44 +++ docs/reference/workflow-fragments.md | 29 ++ gsd-core/references/debugger-techniques.md | 255 +++++++++++++++++ .../references/verifier-wiring-patterns.md | 100 +++++++ src/install-profiles.cts | 31 ++- src/runtime-artifact-layout.cts | 25 +- .../agent-fragments-emission.install.test.cjs | 227 +++++++++++++++ .../agent-marker-documentation-guard.test.cjs | 148 ++++++++++ tests/fixtures/install-tree/antigravity.json | 2 + tests/fixtures/install-tree/augment.json | 2 + tests/fixtures/install-tree/claude-local.json | 2 + tests/fixtures/install-tree/claude.json | 2 + tests/fixtures/install-tree/cline.json | 2 + tests/fixtures/install-tree/codebuddy.json | 2 + tests/fixtures/install-tree/codex.json | 2 + tests/fixtures/install-tree/copilot.json | 2 + tests/fixtures/install-tree/cursor.json | 2 + tests/fixtures/install-tree/hermes.json | 2 + tests/fixtures/install-tree/kilo.json | 2 + tests/fixtures/install-tree/kimi-code.json | 2 + tests/fixtures/install-tree/kimi.json | 2 + tests/fixtures/install-tree/opencode.json | 2 + tests/fixtures/install-tree/pi.json | 2 + tests/fixtures/install-tree/qwen.json | 2 + tests/fixtures/install-tree/trae.json | 2 + tests/fixtures/install-tree/windsurf.json | 2 + tests/fixtures/install-tree/zcode.json | 2 + 34 files changed, 946 insertions(+), 324 deletions(-) create mode 100644 .changeset/agile-pumas-gather.md create mode 100644 gsd-core/references/debugger-techniques.md create mode 100644 gsd-core/references/verifier-wiring-patterns.md create mode 100644 tests/agent-fragments-emission.install.test.cjs create mode 100644 tests/agent-marker-documentation-guard.test.cjs diff --git a/.changeset/agile-pumas-gather.md b/.changeset/agile-pumas-gather.md new file mode 100644 index 000000000..b6dbc7e69 --- /dev/null +++ b/.changeset/agile-pumas-gather.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 3058 +--- +**Agent definitions now share the workflow fragment pipeline** — a `` marker in an `agents/*.md` file is stripped at install time on every emission path instead of shipping verbatim into the runtime, and the largest agents move their reference material into `gsd-core/references/` so they regain headroom under their size caps. (#2995) diff --git a/CONTEXT.md b/CONTEXT.md index c54cd8189..6994fe810 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -207,11 +207,11 @@ Shared, pure, no-I/O seam owning priority-ordered composition of content fragmen ### Workflow Fragments Module -Pure, no-I/O seam owning in-file `` / `` marker parsing and composition for GSD workflow markdown (ADR-1671 Decision item 1 + migration step 4 + open questions 1 & 2; epic #1671 Phase 3, #2930). `parseWorkflowSections` partitions a document into explicit (marked) and gap (unmarked, `explicit: false`) sections in document order — a marker line is removed in full (text + its own terminator), so an unmarked workflow (88 of 89 today) parses to exactly one implicit gap fragment and round-trips byte-identical. `toFragments` maps sections to Context Composer Module fragments, every one `{kind: 'verbatim'}` — non-lossiness in this phase is a structural guarantee of the strategy set, never a large-budget trick. `composeWorkflow` is the emission entry point: parse → `toFragments` → `composeWithinBudget` → `renderFragments`, run BEFORE the per-runtime converters so a marker attribute is stripped before any path-rewrite regex can reach it. **The grammar is deliberately CLOSED** (Greenspun's Tenth Rule): `when=` takes exactly one atom from the frozen `WHEN_VOCABULARY` — `always`, `flag:--wave`, `state:gap-closure-phase`, `state:has-prior-phases` — with no boolean operators, negation, or nesting; an unknown `when=` value throws rather than being silently dropped, and widening the vocabulary requires an ADR amendment, not an organic edit. Fence and HTML-comment interleaving is scanned in one left-to-right pass with two mutually exclusive states, reusing the discipline from the Context Predicates module's fence/comment scan (the two-pass design that caused #2928's silent-skip-to-EOF defect). `when=` is parsed and validated but not yet acted on — applicability selection is Phase 5; this phase lands the authoring model and proves the seam on one pilot workflow (`execute-phase.md`; retargeted from `plan-phase.md`, which sits 36 B under the ADR-857 `PRE_PHASE6` gate and cannot absorb marker overhead). Source of truth: `gsd-core/bin/lib/workflow-fragments.cjs` (generated from `src/workflow-fragments.cts`). Test anchors: `tests/workflow-fragments.test.cjs`, `tests/workflow-fragments.property.test.cjs`, `tests/workflow-fragments-emission.install.test.cjs`. +Pure, no-I/O seam owning in-file `` / `` marker parsing and composition for GSD workflow markdown (ADR-1671 Decision item 1 + migration step 4 + open questions 1 & 2; epic #1671 Phase 3, #2930). `parseWorkflowSections` partitions a document into explicit (marked) and gap (unmarked, `explicit: false`) sections in document order — a marker line is removed in full (text + its own terminator), so an unmarked workflow (88 of 89 today) parses to exactly one implicit gap fragment and round-trips byte-identical. `toFragments` maps sections to Context Composer Module fragments, every one `{kind: 'verbatim'}` — non-lossiness in this phase is a structural guarantee of the strategy set, never a large-budget trick. `composeWorkflow` is the emission entry point: parse → `toFragments` → `composeWithinBudget` → `renderFragments`, run BEFORE the per-runtime converters so a marker attribute is stripped before any path-rewrite regex can reach it. **The grammar is deliberately CLOSED** (Greenspun's Tenth Rule): `when=` takes exactly one atom from the frozen `WHEN_VOCABULARY` — **29 atoms** (4 at #2930, widened 4→14 by #2992, 14→19 by #2993, 19→29 by #2994, each requiring a coordinated ADR-1671 amendment) — with no boolean operators, negation, or nesting; an unknown `when=` value throws rather than being silently dropped, and widening the vocabulary requires an ADR amendment, not an organic edit. Two gates govern admission: a named consuming section of at least 400 bytes, and a fact the init seam demonstrably computes at a real entry point — an atom failing the second evaluates `false` forever, so its marker looks like working gating while silently disabling itself. Any condition that cannot reduce to a single boolean is not an atom: a compound is resolved upstream in the FACT by the init seam (`state:chunked-mode`), never in the grammar, and negation is expressed as its own positively-phrased atom (`state:flat-mode`), never as an operator. Fence and HTML-comment interleaving is scanned in one left-to-right pass with two mutually exclusive states, reusing the discipline from the Context Predicates module's fence/comment scan (the two-pass design that caused #2928's silent-skip-to-EOF defect). `when=` was parsed and validated but not acted on at #2930; applicability selection shipped in Phase 5 (#2932, Section Manifest Module) and the rollout is complete across **15 workflows / 37 sections** (pilot `execute-phase.md` at #2930 — retargeted from `plan-phase.md`, which sat 36 B under the ADR-857 `PRE_PHASE6` gate and could not absorb marker overhead until #2993 fragmentized it; the remaining 13 LARGE/XL workflows at #2994). **Emission scope covers `agents/` as of #2995**, so a marker in an agent file is stripped at emit rather than shipped verbatim into every runtime — composition runs at two call sites (`stageAgentsForRuntimeWithConverter`, with `agentsKind`/`kimiAgentsKind` routed through it, and `bin/install.js`'s inline agent loop) plus `installCodexConfig`'s per-agent TOML read, always BEFORE any path rewrite. **`when=` GATING remains workflow-only**: `section-manifest.json` is keyed `{workflows: …}` and `gen-section-manifest.cjs` scans only `gsd-core/workflows/*.md`, so an agent atom has no consumer and would fail admission gate (2); agents are size-managed by extraction to `gsd-core/references/` per `DEFECT.AGENT-FILE-SIZE-CAP-BREACH` instead. Source of truth: `gsd-core/bin/lib/workflow-fragments.cjs` (generated from `src/workflow-fragments.cts`). Test anchors: `tests/workflow-fragments.test.cjs`, `tests/workflow-fragments.property.test.cjs`, `tests/workflow-fragments-emission.install.test.cjs`. ### Section Manifest Module -Pure, no-I/O `when=` evaluator over `InvocationFacts`, mapping a document-order list of parsed `gsd:section` sections (Workflow Fragments Module) to an included/excluded partition for one concrete invocation (ADR-1671 Decision items 3 & 4 + migration step 6; epic #1671 Phase 5, #2932). **The evaluator is a LOOKUP, not a parser** — `WHEN_PREDICATES` is a total map from each frozen `WHEN_VOCABULARY` entry (imported unchanged from `workflow-fragments.cjs`, never redeclared) to exactly one predicate over `InvocationFacts = {waveFlag, phaseNumber, hasPriorPhases}`; it MUST NOT tokenize, split on operators, or interpret `when=` structure — the moment it parses, the ad-hoc language Greenspun's Tenth Rule warns against has begun. `selectSections(sections, facts)` returns `{included, excluded}` id arrays that together contain every input id exactly once, in the same relative document order, never mutating the input. An unrecognized `when=` value fails closed via a `TypeError` carrying `.reason = REASON.UNKNOWN_WHEN` — never silently excluded — matching the discipline Phase 3 already established for the same vocabulary at parse time. Every predicate treats an absent fact key as falsy without throwing, since the caller (the init CLI seam) may not always populate every field. A coordinated-change guard runs at module load: every `WHEN_VOCABULARY` entry must have exactly one predicate here, so a 5th vocabulary entry added without a matching predicate fails loudly at load time rather than silently falling through to `REASON.UNKNOWN_WHEN` only at run time. Selection output is generated ahead of time into the committed `gsd-core/workflows/section-manifest.json` (`scripts/gen-section-manifest.cjs`, reusing `parseWorkflowSections` unchanged — a second marker parser here would be the `DEFECT.GENERATIVE-FIX` divergence class) rather than derived from markers at run time, because markers are stripped at emit and the installed parent carries no `gsd:section` metadata. Source of truth: `gsd-core/bin/lib/section-manifest.cjs` (generated from `src/section-manifest.cts`). Test anchors: `tests/section-manifest.test.cjs`, `tests/section-manifest.property.test.cjs`, `tests/gen-section-manifest.test.cjs`. +Pure, no-I/O `when=` evaluator over `InvocationFacts`, mapping a document-order list of parsed `gsd:section` sections (Workflow Fragments Module) to an included/excluded partition for one concrete invocation (ADR-1671 Decision items 3 & 4 + migration step 6; epic #1671 Phase 5, #2932). **The evaluator is a LOOKUP, not a parser** — `WHEN_PREDICATES` is a total map from each frozen `WHEN_VOCABULARY` entry (imported unchanged from `workflow-fragments.cjs`, never redeclared) to exactly one predicate over `InvocationFacts` — `{flags: ReadonlySet, phaseNumber: string|null, hasPriorPhases: boolean}` plus optional already-resolved booleans (`needsCodebaseMap`, `phaseMvpMode`, `worktreesEnabled`, `chunkedMode`, `uiPhaseActive`, `fallowEnabled`, …), every field a plain value the caller computed before `selectSections` runs; `flags` is a `ReadonlySet` rather than a plain object because `.has()` carries no prototype hazard — and note `parseNamedArgs` NEVER returns `undefined` for an absent flag (booleans come back `false`, value keys `null`), so "present in the options record" is not token presence. It MUST NOT tokenize, split on operators, or interpret `when=` structure — the moment it parses, the ad-hoc language Greenspun's Tenth Rule warns against has begun. `selectSections(sections, facts)` returns `{included, excluded}` id arrays that together contain every input id exactly once, in the same relative document order, never mutating the input. An unrecognized `when=` value fails closed via a `TypeError` carrying `.reason = REASON.UNKNOWN_WHEN` — never silently excluded — matching the discipline Phase 3 already established for the same vocabulary at parse time. Every predicate treats an absent fact key as falsy without throwing, since the caller (the init CLI seam) may not always populate every field. A coordinated-change guard runs at module load: every `WHEN_VOCABULARY` entry must have exactly one predicate here, so a 5th vocabulary entry added without a matching predicate fails loudly at load time rather than silently falling through to `REASON.UNKNOWN_WHEN` only at run time. Selection output is generated ahead of time into the committed `gsd-core/workflows/section-manifest.json` (`scripts/gen-section-manifest.cjs`, reusing `parseWorkflowSections` unchanged — a second marker parser here would be the `DEFECT.GENERATIVE-FIX` divergence class) rather than derived from markers at run time, because markers are stripped at emit and the installed parent carries no `gsd:section` metadata. Source of truth: `gsd-core/bin/lib/section-manifest.cjs` (generated from `src/section-manifest.cts`). Test anchors: `tests/section-manifest.test.cjs`, `tests/section-manifest.property.test.cjs`, `tests/gen-section-manifest.test.cjs`. ### Runtime Artifact Layout Module Module owning the per-runtime mapping from artifact kind to filesystem placement. ADR-3660 defines the typed `kinds` per runtime (`commands`, `agents`, `skills`) with destination subpath, prefix, and stage adapter (with per-runtime converters in `bin/install.js`: `convertClaudeCommandToClaudeSkill`, `…CodexSkill`, `…CopilotSkill`, `…AntigravitySkill`). Owns the per-runtime `nested` skill-bundle decision (#69): a `skillsKind` flag in `src/runtime-artifact-layout.cts` drives whether a runtime receives the nested router layout (6 `gsd-ns-*` routers + concrete skills under `/skills//`) or the flat `skills/gsd-/` layout; the evidence/doc-link matrix is recorded in a comment above `resolveRuntimeArtifactLayout`. Phase 1 applies this seam to the Runtime Surface Module (`surface.cjs:applySurface`); as of #813, `applySurface` applies the same per-runtime skill-body path rewrites as `installRuntimeArtifacts` for `skills` kinds — re-surfacing no longer overwrites installed SKILL.md bodies with converter-default `~/.claude` paths. Per ADR-1508 / #1511 the former `getInstallExports`/`loadInstallExports` relay (a `GSD_TEST_MODE`-guarded `require('bin/install.js')` by which `surface.cjs` reached `computePathPrefix`/`applyRuntimeContentRewritesInPlace`) was DELETED from this module; content rewriting now lives in the Runtime Artifact Conversion Module and `surface.cjs:applySurface` calls its `rewriteStagedSkillBodies` directly. The resolved `scope` is still carried on the `Layout` object so `applySurface` derives the same `pathPrefix` (global `$HOME` form vs. absolute) as a fresh install. Phase 2 is planned to migrate install/uninstall in `bin/install.js` so all lifecycle sites iterate one shared layout table instead of re-encoding runtime layout logic. This design is intended to remove the #3659 class of omissions. Migrations remain under the Installer Migration Module (ADR-0008). The `.gsd-source` marker (#1477) is a two-party provisioning contract that lets source resolution succeed on the Claude global skills layout, which ships `gsd-core/{bin,contexts,references,templates,workflows}` but no `commands/gsd` source tree for `findInstallSourceRoot` to walk up to: the writer is `bin/install.js`, which writes `/.gsd-source` (content: the absolute path to its own `commands/gsd`, terminated by a newline) when `runtime === 'claude' && isGlobal`, guarded by `fs.existsSync` so a half-published package never writes a dangling marker; the reader is `findInstallSourceRoot(configDir)`, which prefers the marker over its walk-up but falls through to the walk-up if the marker is absent, dangling, or empty/whitespace-only. See ADR-3660. diff --git a/agents/gsd-debugger.md b/agents/gsd-debugger.md index f5f9cf57f..3a84f2a69 100644 --- a/agents/gsd-debugger.md +++ b/agents/gsd-debugger.md @@ -168,73 +168,20 @@ try { -## Binary Search / Divide and Conquer +## Technique Catalog -**When:** Large codebase, long execution path, many possible failure points. +Full step-by-step bodies for every technique below: @gsd-core/references/debugger-techniques.md -**How:** Cut problem space in half repeatedly until you isolate the issue. - -1. Identify boundaries (where works, where fails) -2. Add logging/testing at midpoint -3. Determine which half contains the bug -4. Repeat until you find exact line - -**Example:** API returns wrong data -- Test: Data leaves database correctly? YES -- Test: Data reaches frontend correctly? NO -- Test: Data leaves API route correctly? YES -- Test: Data survives serialization? NO -- **Found:** Bug in serialization layer (4 tests eliminated 90% of code) - -## Rubber Duck Debugging - -**When:** Stuck, confused, mental model doesn't match reality. - -**How:** Explain the problem out loud in complete detail. - -Write or say: -1. "The system should do X" -2. "Instead it does Y" -3. "I think this is because Z" -4. "The code path is: A -> B -> C -> D" -5. "I've verified that..." (list what you tested) -6. "I'm assuming that..." (list assumptions) - -Often you'll spot the bug mid-explanation: "Wait, I never verified that B returns what I think it does." - -## Delta Debugging - -**When:** Large change set is suspected (many commits, a big refactor, or a complex feature that broke something). Also when "comment out everything" is too slow. - -**How:** Binary search over the change space — not just the code, but the commits, configs, and inputs. - -**Over commits (use git bisect):** -Already covered under Git Bisect. But delta debugging extends it: after finding the breaking commit, delta-debug the commit itself — identify which of its N changed files/lines actually causes the failure. - -**Over code (systematic elimination):** -1. Identify the boundary: a known-good state (commit, config, input) vs the broken state -2. List all differences between good and bad states -3. Split the differences in half. Apply only half to the good state. -4. If broken: bug is in the applied half. If not: bug is in the other half. -5. Repeat until you have the minimal change set that causes the failure. - -**Over inputs:** -1. Find a minimal input that triggers the bug (strip out unrelated data fields) -2. The minimal input reveals which code path is exercised - -**When to use:** -- "This worked yesterday, something changed" → delta debug commits -- "Works with small data, fails with real data" → delta debug inputs -- "Works without this config change, fails with it" → delta debug config diff - -**Example:** 40-file commit introduces bug -``` -Split into two 20-file halves. -Apply first 20: still works → bug in second half. -Split second half into 10+10. -Apply first 10: broken → bug in first 10. -... 6 splits later: single file isolated. -``` +- **Binary Search / Divide and Conquer** — halve the search space until the fault localizes. +- **Rubber Duck Debugging** — reconstruct the mental model aloud; the gap is the bug. +- **Delta Debugging** — shrink a failing input to its minimal failing core. +- **Minimal Reproduction** — strip everything not required to reproduce. +- **Working Backwards** — start at the symptom and walk causality in reverse. +- **Differential Debugging** — compare a working case against a failing one. +- **Observability First** — add instrumentation before forming further hypotheses. +- **Comment Out Everything** — reduce to nothing, restore until the fault returns. +- **Git Bisect** — binary-search history for the introducing commit. +- **Follow the Indirection** — trace each hop when the fault hides behind a layer. ## Structured Reasoning Checkpoint @@ -268,187 +215,6 @@ reasoning_checkpoint: If you cannot fill all seven fields with specific, concrete answers — you do not have a confirmed root cause yet. Return to investigation_loop. -## Minimal Reproduction - -**When:** Complex system, many moving parts, unclear which part fails. - -**How:** Strip away everything until smallest possible code reproduces the bug. - -1. Copy failing code to new file -2. Remove one piece (dependency, function, feature) -3. Test: Does it still reproduce? YES = keep removed. NO = put back. -4. Repeat until bare minimum -5. Bug is now obvious in stripped-down code -6. **Shrinking (input-space bugs)** — when the bug triggers on a class of inputs, wrap it in a property (fast-check for JS/TS, Hypothesis for Python) and let the shrinker auto-minimize the counterexample; store the **minimized** input as the regression seed. See `gsd-core/references/debugger-repro-hardening.md`. - -**Example:** -```jsx -// Start: 500-line React component with 15 props, 8 hooks, 3 contexts -// End after stripping: -function MinimalRepro() { - const [count, setCount] = useState(0); - - useEffect(() => { - setCount(count + 1); // Bug: infinite loop, missing dependency array - }); - - return
{count}
; -} -// The bug was hidden in complexity. Minimal reproduction made it obvious. -``` - -## Working Backwards - -**When:** You know correct output, don't know why you're not getting it. - -**How:** Start from desired end state, trace backwards. - -1. Define desired output precisely -2. What function produces this output? -3. Test that function with expected input - does it produce correct output? - - YES: Bug is earlier (wrong input) - - NO: Bug is here -4. Repeat backwards through call stack -5. Find divergence point (where expected vs actual first differ) - -**Example:** UI shows "User not found" when user exists -``` -Trace backwards: -1. UI displays: user.error → Is this the right value to display? YES -2. Component receives: user.error = "User not found" → Correct? NO, should be null -3. API returns: { error: "User not found" } → Why? -4. Database query: SELECT * FROM users WHERE id = 'undefined' → AH! -5. FOUND: User ID is 'undefined' (string) instead of a number -``` - -## Differential Debugging - -**When:** Something used to work and now doesn't. Works in one environment but not another. - -**Time-based (worked, now doesn't):** -- What changed in code since it worked? -- What changed in environment? (Node version, OS, dependencies) -- What changed in data? -- What changed in configuration? - -**Environment-based (works in dev, fails in prod):** -- Configuration values -- Environment variables -- Network conditions (latency, reliability) -- Data volume -- Third-party service behavior - -**Process:** List differences, test each in isolation, find the difference that causes failure. - -**Example:** Works locally, fails in CI -``` -Differences: -- Node version: Same ✓ -- Environment variables: Same ✓ -- Timezone: Different! ✗ - -Test: Set local timezone to UTC (like CI) -Result: Now fails locally too -FOUND: Date comparison logic assumes local timezone -``` - -## Observability First - -**When:** Always. Before making any fix. - -**Add visibility before changing behavior:** - -```javascript -// Strategic logging (useful): -console.log('[handleSubmit] Input:', { email, password: '***' }); -console.log('[handleSubmit] Validation result:', validationResult); -console.log('[handleSubmit] API response:', response); - -// Assertion checks: -console.assert(user !== null, 'User is null!'); -console.assert(user.id !== undefined, 'User ID is undefined!'); - -// Timing measurements: -console.time('Database query'); -const result = await db.query(sql); -console.timeEnd('Database query'); - -// Stack traces at key points: -console.log('[updateUser] Called from:', new Error().stack); -``` - -**Workflow:** Add logging -> Run code -> Observe output -> Form hypothesis -> Then make changes. - -## Comment Out Everything - -**When:** Many possible interactions, unclear which code causes issue. - -**How:** -1. Comment out everything in function/file -2. Verify bug is gone -3. Uncomment one piece at a time -4. After each uncomment, test -5. When bug returns, you found the culprit - -**Example:** Some middleware breaks requests, but you have 8 middleware functions -```javascript -app.use(helmet()); // Uncomment, test → works -app.use(cors()); // Uncomment, test → works -app.use(compression()); // Uncomment, test → works -app.use(bodyParser.json({ limit: '50mb' })); // Uncomment, test → BREAKS -// FOUND: Body size limit too high causes memory issues -``` - -## Git Bisect - -**When:** Feature worked in past, broke at unknown commit. - -**How:** Binary search through git history. - -```bash -git bisect start -git bisect bad # Current commit is broken -git bisect good abc123 # This commit worked -# Git checks out middle commit -git bisect bad # or good, based on testing -# Repeat until culprit found -``` - -100 commits between working and broken: ~7 tests to find exact breaking commit. - -## Follow the Indirection - -**When:** Code constructs paths, URLs, keys, or references from variables — and the constructed value might not point where you expect. - -**The trap:** You read code that builds a path like `path.join(configDir, 'hooks')` and assume it's correct because it looks reasonable. But you never verified that the constructed path matches where another part of the system actually writes/reads. - -**How:** -1. Find the code that **produces** the value (writer/installer/creator) -2. Find the code that **consumes** the value (reader/checker/validator) -3. Trace the actual resolved value in both — do they agree? -4. Check every variable in the path construction — where does each come from? What's its actual value at runtime? - -**Common indirection bugs:** -- Path A writes to `dir/sub/hooks/` but Path B checks `dir/hooks/` (directory mismatch) -- Config value comes from cache/template that wasn't updated -- Variable is derived differently in two places (e.g., one adds a subdirectory, the other doesn't) -- Template placeholder (`{{VERSION}}`) not substituted in all code paths - -**Example:** Stale hook warning persists after update -``` -Check code says: hooksDir = path.join(configDir, 'hooks') - configDir = ~/.claude - → checks ~/.claude/hooks/ - -Installer says: hooksDest = path.join(targetDir, 'hooks') - targetDir = ~/.claude/gsd-core - → writes to ~/.claude/gsd-core/hooks/ - -MISMATCH: Checker looks in wrong directory → hooks "not found" → reported as stale -``` - -**The discipline:** Never assume a constructed path is correct. Resolve it to its actual value and verify the other side agrees. When two systems share a resource (file, directory, key), trace the full path in both. - ## Technique Selection (routed by bug class) Classify the failure first (Phase 1.75), then route by class — not by ad-hoc diff --git a/agents/gsd-verifier.md b/agents/gsd-verifier.md index d21832b5d..6894aba9a 100644 --- a/agents/gsd-verifier.md +++ b/agents/gsd-verifier.md @@ -285,46 +285,16 @@ grep -r "$artifact_name" "${search_path:-src/}" --include="*.ts" --include="*.ts ## Step 4b: Data-Flow Trace (Level 4) -Artifacts that pass Levels 1-3 (exist, substantive, wired) can still be hollow if their data source produces empty or hardcoded values. Level 4 traces upstream from the artifact to verify real data flows through the wiring. +Trace each rendered value back to a real data source. Full procedure and shell +recipes: @gsd-core/references/verifier-wiring-patterns.md -**When to run:** For each artifact that passes Level 3 (WIRED) and renders dynamic data (components, pages, dashboards — not utilities or configs). +Flag any value whose chain terminates in a static return, a hardcoded literal, or +a mock rather than a real query. -**How:** +**Data-flow status vocabulary:** -1. **Identify the data variable** — what state/prop does the artifact render? - -```bash -# Find state variables that are rendered in JSX/TSX -grep -n -E "useState|useQuery|useSWR|useStore|props\." "$artifact" 2>/dev/null -``` - -2. **Trace the data source** — where does that variable get populated? - -```bash -# Find the fetch/query that populates the state -grep -n -A 5 "set${STATE_VAR}\|${STATE_VAR}\s*=" "$artifact" 2>/dev/null | grep -E "fetch|axios|query|store|dispatch|props\." -``` - -3. **Verify the source produces real data** — does the API/store return actual data or static/empty values? - -```bash -# Check the API route or data source for real DB queries vs static returns -grep -n -E "prisma\.|db\.|query\(|findMany|findOne|select|FROM" "$source_file" 2>/dev/null -# Flag: static returns with no query -grep -n -E "return.*json\(\s*\[\]|return.*json\(\s*\{\}" "$source_file" 2>/dev/null -``` - -4. **Check for disconnected props** — props passed to child components that are hardcoded empty at the call site - -```bash -# Find where the component is used and check prop values -grep -r -A 3 "<${COMPONENT_NAME}" "${search_path:-src/}" --include="*.tsx" 2>/dev/null | grep -E "=\{(\[\]|\{\}|null|''|\"\")\}" -``` - -**Data-flow status:** - -| Data Source | Produces Real Data | Status | -| ---------- | ------------------ | ------ | +| Data source | Flows | Status | +| ----------- | ----- | ------ | | DB query found | Yes | ✓ FLOWING | | Fetch exists, static fallback only | No | ⚠️ STATIC | | No data source found | N/A | ✗ DISCONNECTED | @@ -359,41 +329,15 @@ For each link: **Fallback patterns** (if must_haves.key_links not defined in PLAN): -### Pattern: Component → API +### Wiring patterns -```bash -grep -E "fetch\(['\"].*$api_path|axios\.(get|post).*$api_path" "$component" 2>/dev/null -grep -A 5 "fetch\|axios" "$component" | grep -E "await|\.then|setData|setState" 2>/dev/null -``` +Verify each link below; full per-pattern procedures and shell recipes: +@gsd-core/references/verifier-wiring-patterns.md -Status: WIRED (call + response handling) | PARTIAL (call, no response use) | NOT_WIRED (no call) - -### Pattern: API → Database - -```bash -grep -E "prisma\.$model|db\.$model|$model\.(find|create|update|delete)" "$route" 2>/dev/null -grep -E "return.*json.*\w+|res\.json\(\w+" "$route" 2>/dev/null -``` - -Status: WIRED (query + result returned) | PARTIAL (query, static return) | NOT_WIRED (no query) - -### Pattern: Form → Handler - -```bash -grep -E "onSubmit=\{|handleSubmit" "$component" 2>/dev/null -grep -A 10 "onSubmit.*=" "$component" | grep -E "fetch|axios|mutate|dispatch" 2>/dev/null -``` - -Status: WIRED (handler + API call) | STUB (only logs/preventDefault) | NOT_WIRED (no handler) - -### Pattern: State → Render - -```bash -grep -E "useState.*$state_var|\[$state_var," "$component" 2>/dev/null -grep -E "\{.*$state_var.*\}|\{$state_var\." "$component" 2>/dev/null -``` - -Status: WIRED (state displayed) | NOT_WIRED (state exists, not rendered) +- **Component → API** — the component actually calls the endpoint it claims. +- **API → Database** — the endpoint issues a real query, not a static return. +- **Form → Handler** — submission reaches a handler that persists. +- **State → Render** — state changes actually reach the rendered output. ## Step 6: Check Requirements Coverage diff --git a/bin/install.js b/bin/install.js index 652672ef1..083302556 100755 --- a/bin/install.js +++ b/bin/install.js @@ -6928,7 +6928,16 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san const codexGsdPath = `${path.resolve(targetDir, 'gsd-core').replace(/\\/g, '/')}/`; for (const file of agentEntries) { - let content = fs.readFileSync(path.join(agentsSrc, file), 'utf8'); + const agentTomlSourcePath = path.join(agentsSrc, file); + let content = fs.readFileSync(agentTomlSourcePath, 'utf8'); + // #2995 (epic #1671 Phase 6.4): Codex embeds each agent's prompt into a + // per-agent `.toml`, reading the source .md independently of the inline + // agent loop — a separate emission path that must strip gsd:section + // markers too, or a marked agent ships its markers inside the TOML. + // Found by the exhaustive per-runtime emission sweep in + // tests/agent-fragments-emission.install.test.cjs, not by call-graph + // analysis, which is why that guard is behavioral rather than structural. + content = composeWorkflow(content, { sourcePath: agentTomlSourcePath }); // Replace full .claude/gsd-core prefix so path resolves to the Codex // GSD install before generic .claude → .codex conversion rewrites it. content = content.replace(/~\/\.claude\/gsd-core\//g, codexGsdPath); @@ -10864,7 +10873,12 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) { const agentEntries = fs.readdirSync(agentsSrc, { withFileTypes: true }); for (const entry of agentEntries) { if (entry.isFile() && entry.name.endsWith('.md')) { - let content = fs.readFileSync(path.join(agentsSrc, entry.name), 'utf8'); + const agentSourcePath = path.join(agentsSrc, entry.name); + let content = fs.readFileSync(agentSourcePath, 'utf8'); + // #2995 (epic #1671 Phase 6.4): strip `` markers BEFORE + // the path-rewrite regexes below, so a rewrite can never reach inside a + // marker attribute. No-op (byte-identical) for an unmarked agent. + content = composeWorkflow(content, { sourcePath: agentSourcePath }); // Replace ~/.claude/ and $HOME/.claude/ as they are the source of truth in the repo const dirRegex = /~\/\.claude\//g; const homeDirRegex = /\$HOME\/\.claude\//g; diff --git a/docs/INVENTORY-MANIFEST.json b/docs/INVENTORY-MANIFEST.json index c95e57702..abc9e561d 100644 --- a/docs/INVENTORY-MANIFEST.json +++ b/docs/INVENTORY-MANIFEST.json @@ -223,6 +223,7 @@ "debugger-repro-hardening.md", "debugger-sbfl.md", "debugger-semantic-recall.md", + "debugger-techniques.md", "decimal-phase-calculation.md", "doc-conflict-engine.md", "domain-probes.md", @@ -297,6 +298,7 @@ "user-story-template.md", "verification-overrides.md", "verification-patterns.md", + "verifier-wiring-patterns.md", "verify-mvp-mode.md", "workstream-flag.md", "worktree-branch-check.md", diff --git a/docs/INVENTORY.md b/docs/INVENTORY.md index 061b501cf..a884d8f0b 100644 --- a/docs/INVENTORY.md +++ b/docs/INVENTORY.md @@ -301,6 +301,8 @@ Full roster at `gsd-core/references/*.md`. References are shared knowledge docum | `debugger-repro-hardening.md` | Regression-test hardening (PBT shrinking + oracle classification + boundary neighbors) loaded by `gsd-debugger`. | | `debugger-prevention.md` | Prevention / blameless-postmortem output (5-Whys + why-not-caught + recurrence guard) loaded by `gsd-debugger`. | | `debugger-semantic-recall.md` | Semantic knowledge-base recall via MemPalace (keyword-fallback) loaded by `gsd-debugger`. | +| `debugger-techniques.md` | Full step-by-step bodies for the 10 debugging techniques (binary search, delta debugging, git bisect, …) routed by `gsd-debugger`'s technique-selection table. | +| `verifier-wiring-patterns.md` | Data-flow trace procedure and the four wiring patterns (Component→API, API→Database, Form→Handler, State→Render) loaded by `gsd-verifier`. | | `mandatory-initial-read.md` | Shared required-reading boilerplate injected into agent prompts. | | `agent-skills-bootstrap.md` | Shared agent_skills self-load contract (query + Read + dedup guard) injected into all 22 consumer agents. | | `project-skills-discovery.md` | Shared project-skills-discovery boilerplate injected into agent prompts. | diff --git a/docs/adr/1671-dynamic-context-management-platform.md b/docs/adr/1671-dynamic-context-management-platform.md index 733c99fa9..af82e968f 100644 --- a/docs/adr/1671-dynamic-context-management-platform.md +++ b/docs/adr/1671-dynamic-context-management-platform.md @@ -142,6 +142,50 @@ Pure Agent Skills (A alone) and pure MCP (D alone) were rejected as the foundati the "Rejected" cases (`--auto`/`--chain`/persisted-config interleaving; negated `--skip-bounce` OR `--gaps` OR NOT(...)) recorded in `.gsd/phase/chore-2993-fragmentize-plan-phase/40-design.md`. + + **Amended by #2994 (Phase 6.3) — the vocabulary widens 19 → 29, third coordinated + amendment.** Rolling the fragment model onto the remaining 13 LARGE/XL workflows + surfaced 10 more atoms, admitted under the same two gates #2992 established. This + amendment is recorded retroactively by #2995 (Phase 6.4): #2994 shipped the atoms + without it, which this bullet's own rule forbids ("Widening the vocabulary requires a + coordinated ADR amendment, not an organic edit"). The gap was found by re-running + `/adr-phase-coverage` against what actually merged. The atoms each satisfy both + admission gates and are not in question; the missing record is. + + **Shipped (10).** `flag:--fix`, `state:auto-advance-active`, `state:fallow-enabled`, + `state:flat-mode`, `state:git-create-tag`, `state:is-monorepo`, `state:next-channel`, + `state:plan-strategy-converge`, `state:reviewer-instances-configured`, + `state:ui-phase-active`, `state:workstream-active`. Compound real-world triggers + (`--converge OR --cross-ai`, `--next OR --rc`, `--auto OR` config, `--discuss OR + --full`) are each resolved to a single boolean in `src/init.cts` before evaluation, so + `when=` still sees one operator-free atom — the `state:chunked-mode` precedent above. + `state:flat-mode` is the positively-phrased inverse of `state:workstream-active`, + because negation is not in the grammar. + + **`flag:--verify-only` is permanently REJECTED, not pending.** #2992 listed it among + six withheld atoms and deferred all six to "the LARGE/XL rollout phase". Five shipped + in #2994. `flag:--verify-only` did not, and will not: `docs-update`'s control flow is + interleaved across three non-contiguous touch-points, so gating one would leave the + other two as raw `$ARGUMENTS` checks. An atom with no genuine consuming section is dead + vocabulary — the rot the frozen list exists to prevent. That disposition was recorded + only in merged PR #3030's body, leaving this ADR still asserting a hand-off that will + never complete; it is recorded here so the withheld list reaches a terminal state. + + **Amended by #2995 (Phase 6.4) — the grammar does NOT extend to `agents/`.** + Migration step 7 names agents alongside workflows. Emission does extend: agent bodies + now pass through `composeWorkflow` on every emission path, so a marker in an agent is + stripped rather than shipped verbatim. **Gating does not.** `when=` selection is + consumed from the committed `gsd-core/workflows/section-manifest.json`, which + `scripts/gen-section-manifest.cjs` derives from `gsd-core/workflows/*.md` only; its + shape is `{workflows: {...}}` and there is no per-agent entry, no per-agent init entry + point, and no consumer that could evaluate an agent's `when=`. An agent atom therefore + fails admission gate (2) — "a fact the init seam demonstrably computes at a real entry + point" — and would be the exact silent-inertness failure that gate exists to prevent: a + marker that looks like working gating while evaluating `false` forever. Agents are + consequently size-managed by extraction to `gsd-core/references/` (the documented + `DEFECT.AGENT-FILE-SIZE-CAP-BREACH` fix-forward), not by `when=` markers. Extending + gating to agents would require a per-agent manifest family and a dispatch-time seam to + read it; that is a separate decision, not an organic edit, and is not taken here. - **Budget unit:** bytes for emission caps (matches `lfByteCount`, deterministic, offline-safe); a token estimate for run-time selection. **Corrected by #2931 (Phase 4) — the Windsurf cap was never load-bearing.** The Context diff --git a/docs/reference/workflow-fragments.md b/docs/reference/workflow-fragments.md index 75b667eeb..3e570754a 100644 --- a/docs/reference/workflow-fragments.md +++ b/docs/reference/workflow-fragments.md @@ -253,6 +253,35 @@ Composition runs **before** the per-runtime converters (the `.claude/` → `.windsurf/`-style path and reference rewrites), so a marker's `id`/`when` attribute text is never exposed to a rewrite regex. +### Emission covers `agents/` too — gating does not + +Since epic #1671 Phase 6.4 (#2995), agent definitions under `agents/` pass +through the same composition step as workflows. A marker in an agent file is +stripped at emit rather than shipped verbatim into the runtime, on every path +that emits agent content: + +| Emission path | Runtimes | +|---|---| +| `stageAgentsForRuntimeWithConverter` (with the raw `agents` kind and the Kimi agent kind routed through it) | the descriptor-driven runtimes, plus `claude` local and `zcode` | +| `bin/install.js`'s inline agent loop | every non-descriptor runtime | +| `installCodexConfig`'s per-agent `.toml` writer | `codex` | + +All three compose **before** any path rewrite, for the same reason workflows do. + +**What does not extend is `when=` gating.** Selection is read from +`gsd-core/workflows/section-manifest.json`, which `gen-section-manifest.cjs` +derives from `gsd-core/workflows/*.md` only — its shape is `{workflows: …}` and +it has no per-agent key. There is no per-agent init entry point either, so an +agent atom has no fact to evaluate against and would fail the vocabulary's +second admission gate ("a fact the init seam demonstrably computes at a real +entry point"). A `when=` on an agent section would therefore evaluate `false` +forever while *looking* like working gating — the precise failure the frozen +vocabulary exists to prevent. + +Agents that need to shed bytes do so by extracting reference material to +`gsd-core/references/` behind an `@`-reference, the documented +`DEFECT.AGENT-FILE-SIZE-CAP-BREACH` remedy — not by adding markers. + ## Fenced and commented lookalikes are literal A ``-shaped line inside a fenced code block (three or diff --git a/gsd-core/references/debugger-techniques.md b/gsd-core/references/debugger-techniques.md new file mode 100644 index 000000000..50794d64a --- /dev/null +++ b/gsd-core/references/debugger-techniques.md @@ -0,0 +1,255 @@ +# Debugger technique catalog + +Full technique bodies for `agents/gsd-debugger.md`, extracted per +`DEFECT.AGENT-FILE-SIZE-CAP-BREACH` (issue #2995, epic #1671 Phase 6.4). The agent +keeps each technique's name and routing entry; the step-by-step detail lives here. + +## Binary Search / Divide and Conquer + +**When:** Large codebase, long execution path, many possible failure points. + +**How:** Cut problem space in half repeatedly until you isolate the issue. + +1. Identify boundaries (where works, where fails) +2. Add logging/testing at midpoint +3. Determine which half contains the bug +4. Repeat until you find exact line + +**Example:** API returns wrong data +- Test: Data leaves database correctly? YES +- Test: Data reaches frontend correctly? NO +- Test: Data leaves API route correctly? YES +- Test: Data survives serialization? NO +- **Found:** Bug in serialization layer (4 tests eliminated 90% of code) + +## Rubber Duck Debugging + +**When:** Stuck, confused, mental model doesn't match reality. + +**How:** Explain the problem out loud in complete detail. + +Write or say: +1. "The system should do X" +2. "Instead it does Y" +3. "I think this is because Z" +4. "The code path is: A -> B -> C -> D" +5. "I've verified that..." (list what you tested) +6. "I'm assuming that..." (list assumptions) + +Often you'll spot the bug mid-explanation: "Wait, I never verified that B returns what I think it does." + +## Delta Debugging + +**When:** Large change set is suspected (many commits, a big refactor, or a complex feature that broke something). Also when "comment out everything" is too slow. + +**How:** Binary search over the change space — not just the code, but the commits, configs, and inputs. + +**Over commits (use git bisect):** +Already covered under Git Bisect. But delta debugging extends it: after finding the breaking commit, delta-debug the commit itself — identify which of its N changed files/lines actually causes the failure. + +**Over code (systematic elimination):** +1. Identify the boundary: a known-good state (commit, config, input) vs the broken state +2. List all differences between good and bad states +3. Split the differences in half. Apply only half to the good state. +4. If broken: bug is in the applied half. If not: bug is in the other half. +5. Repeat until you have the minimal change set that causes the failure. + +**Over inputs:** +1. Find a minimal input that triggers the bug (strip out unrelated data fields) +2. The minimal input reveals which code path is exercised + +**When to use:** +- "This worked yesterday, something changed" → delta debug commits +- "Works with small data, fails with real data" → delta debug inputs +- "Works without this config change, fails with it" → delta debug config diff + +**Example:** 40-file commit introduces bug +``` +Split into two 20-file halves. +Apply first 20: still works → bug in second half. +Split second half into 10+10. +Apply first 10: broken → bug in first 10. +... 6 splits later: single file isolated. +``` + +## Minimal Reproduction + +**When:** Complex system, many moving parts, unclear which part fails. + +**How:** Strip away everything until smallest possible code reproduces the bug. + +1. Copy failing code to new file +2. Remove one piece (dependency, function, feature) +3. Test: Does it still reproduce? YES = keep removed. NO = put back. +4. Repeat until bare minimum +5. Bug is now obvious in stripped-down code +6. **Shrinking (input-space bugs)** — when the bug triggers on a class of inputs, wrap it in a property (fast-check for JS/TS, Hypothesis for Python) and let the shrinker auto-minimize the counterexample; store the **minimized** input as the regression seed. See `gsd-core/references/debugger-repro-hardening.md`. + +**Example:** +```jsx +// Start: 500-line React component with 15 props, 8 hooks, 3 contexts +// End after stripping: +function MinimalRepro() { + const [count, setCount] = useState(0); + + useEffect(() => { + setCount(count + 1); // Bug: infinite loop, missing dependency array + }); + + return
{count}
; +} +// The bug was hidden in complexity. Minimal reproduction made it obvious. +``` + +## Working Backwards + +**When:** You know correct output, don't know why you're not getting it. + +**How:** Start from desired end state, trace backwards. + +1. Define desired output precisely +2. What function produces this output? +3. Test that function with expected input - does it produce correct output? + - YES: Bug is earlier (wrong input) + - NO: Bug is here +4. Repeat backwards through call stack +5. Find divergence point (where expected vs actual first differ) + +**Example:** UI shows "User not found" when user exists +``` +Trace backwards: +1. UI displays: user.error → Is this the right value to display? YES +2. Component receives: user.error = "User not found" → Correct? NO, should be null +3. API returns: { error: "User not found" } → Why? +4. Database query: SELECT * FROM users WHERE id = 'undefined' → AH! +5. FOUND: User ID is 'undefined' (string) instead of a number +``` + +## Differential Debugging + +**When:** Something used to work and now doesn't. Works in one environment but not another. + +**Time-based (worked, now doesn't):** +- What changed in code since it worked? +- What changed in environment? (Node version, OS, dependencies) +- What changed in data? +- What changed in configuration? + +**Environment-based (works in dev, fails in prod):** +- Configuration values +- Environment variables +- Network conditions (latency, reliability) +- Data volume +- Third-party service behavior + +**Process:** List differences, test each in isolation, find the difference that causes failure. + +**Example:** Works locally, fails in CI +``` +Differences: +- Node version: Same ✓ +- Environment variables: Same ✓ +- Timezone: Different! ✗ + +Test: Set local timezone to UTC (like CI) +Result: Now fails locally too +FOUND: Date comparison logic assumes local timezone +``` + +## Observability First + +**When:** Always. Before making any fix. + +**Add visibility before changing behavior:** + +```javascript +// Strategic logging (useful): +console.log('[handleSubmit] Input:', { email, password: '***' }); +console.log('[handleSubmit] Validation result:', validationResult); +console.log('[handleSubmit] API response:', response); + +// Assertion checks: +console.assert(user !== null, 'User is null!'); +console.assert(user.id !== undefined, 'User ID is undefined!'); + +// Timing measurements: +console.time('Database query'); +const result = await db.query(sql); +console.timeEnd('Database query'); + +// Stack traces at key points: +console.log('[updateUser] Called from:', new Error().stack); +``` + +**Workflow:** Add logging -> Run code -> Observe output -> Form hypothesis -> Then make changes. + +## Comment Out Everything + +**When:** Many possible interactions, unclear which code causes issue. + +**How:** +1. Comment out everything in function/file +2. Verify bug is gone +3. Uncomment one piece at a time +4. After each uncomment, test +5. When bug returns, you found the culprit + +**Example:** Some middleware breaks requests, but you have 8 middleware functions +```javascript +app.use(helmet()); // Uncomment, test → works +app.use(cors()); // Uncomment, test → works +app.use(compression()); // Uncomment, test → works +app.use(bodyParser.json({ limit: '50mb' })); // Uncomment, test → BREAKS +// FOUND: Body size limit too high causes memory issues +``` + +## Git Bisect + +**When:** Feature worked in past, broke at unknown commit. + +**How:** Binary search through git history. + +```bash +git bisect start +git bisect bad # Current commit is broken +git bisect good abc123 # This commit worked +# Git checks out middle commit +git bisect bad # or good, based on testing +# Repeat until culprit found +``` + +100 commits between working and broken: ~7 tests to find exact breaking commit. + +## Follow the Indirection + +**When:** Code constructs paths, URLs, keys, or references from variables — and the constructed value might not point where you expect. + +**The trap:** You read code that builds a path like `path.join(configDir, 'hooks')` and assume it's correct because it looks reasonable. But you never verified that the constructed path matches where another part of the system actually writes/reads. + +**How:** +1. Find the code that **produces** the value (writer/installer/creator) +2. Find the code that **consumes** the value (reader/checker/validator) +3. Trace the actual resolved value in both — do they agree? +4. Check every variable in the path construction — where does each come from? What's its actual value at runtime? + +**Common indirection bugs:** +- Path A writes to `dir/sub/hooks/` but Path B checks `dir/hooks/` (directory mismatch) +- Config value comes from cache/template that wasn't updated +- Variable is derived differently in two places (e.g., one adds a subdirectory, the other doesn't) +- Template placeholder (`{{VERSION}}`) not substituted in all code paths + +**Example:** Stale hook warning persists after update +``` +Check code says: hooksDir = path.join(configDir, 'hooks') + configDir = ~/.claude + → checks ~/.claude/hooks/ + +Installer says: hooksDest = path.join(targetDir, 'hooks') + targetDir = ~/.claude/gsd-core + → writes to ~/.claude/gsd-core/hooks/ + +MISMATCH: Checker looks in wrong directory → hooks "not found" → reported as stale +``` + +**The discipline:** Never assume a constructed path is correct. Resolve it to its actual value and verify the other side agrees. When two systems share a resource (file, directory, key), trace the full path in both. + diff --git a/gsd-core/references/verifier-wiring-patterns.md b/gsd-core/references/verifier-wiring-patterns.md new file mode 100644 index 000000000..43d251906 --- /dev/null +++ b/gsd-core/references/verifier-wiring-patterns.md @@ -0,0 +1,100 @@ +# Verifier wiring and data-flow patterns + +Full pattern bodies for `agents/gsd-verifier.md`, extracted per +`DEFECT.AGENT-FILE-SIZE-CAP-BREACH` (issue #2995, epic #1671 Phase 6.4). The agent +keeps the step and its checklist; the per-pattern detail and shell recipes live here. + +Artifacts that pass Levels 1-3 (exist, substantive, wired) can still be hollow if their data source produces empty or hardcoded values. Level 4 traces upstream from the artifact to verify real data flows through the wiring. + +**When to run:** For each artifact that passes Level 3 (WIRED) and renders dynamic data (components, pages, dashboards — not utilities or configs). + +**How:** + +1. **Identify the data variable** — what state/prop does the artifact render? + +```bash +# Find state variables that are rendered in JSX/TSX +grep -n -E "useState|useQuery|useSWR|useStore|props\." "$artifact" 2>/dev/null +``` + +2. **Trace the data source** — where does that variable get populated? + +```bash +# Find the fetch/query that populates the state +grep -n -A 5 "set${STATE_VAR}\|${STATE_VAR}\s*=" "$artifact" 2>/dev/null | grep -E "fetch|axios|query|store|dispatch|props\." +``` + +3. **Verify the source produces real data** — does the API/store return actual data or static/empty values? + +```bash +# Check the API route or data source for real DB queries vs static returns +grep -n -E "prisma\.|db\.|query\(|findMany|findOne|select|FROM" "$source_file" 2>/dev/null +# Flag: static returns with no query +grep -n -E "return.*json\(\s*\[\]|return.*json\(\s*\{\}" "$source_file" 2>/dev/null +``` + +4. **Check for disconnected props** — props passed to child components that are hardcoded empty at the call site + +```bash +# Find where the component is used and check prop values +grep -r -A 3 "<${COMPONENT_NAME}" "${search_path:-src/}" --include="*.tsx" 2>/dev/null | grep -E "=\{(\[\]|\{\}|null|''|\"\")\}" +``` + +> These two status tables are intentionally mirrored in `agents/gsd-verifier.md`. +> The status vocabulary is load-bearing verifier output and must remain in the +> agent body (#2995); this copy is here so the procedure below reads standalone. + +**Data-flow status:** + +| Data Source | Produces Real Data | Status | +| ---------- | ------------------ | ------ | +| DB query found | Yes | ✓ FLOWING | +| Fetch exists, static fallback only | No | ⚠️ STATIC | +| No data source found | N/A | ✗ DISCONNECTED | +| Props hardcoded empty at call site | No | ✗ HOLLOW_PROP | + +**Final Artifact Status (updated with Level 4):** + +| Exists | Substantive | Wired | Data Flows | Status | +| ------ | ----------- | ----- | ---------- | ------ | +| ✓ | ✓ | ✓ | ✓ | ✓ VERIFIED | +| ✓ | ✓ | ✓ | ✗ | ⚠️ HOLLOW — wired but data disconnected | +| ✓ | ✓ | ✗ | - | ⚠️ ORPHANED | +| ✓ | ✗ | - | - | ✗ STUB | +| ✗ | - | - | - | ✗ MISSING | + +### Pattern: Component → API + +```bash +grep -E "fetch\(['\"].*$api_path|axios\.(get|post).*$api_path" "$component" 2>/dev/null +grep -A 5 "fetch\|axios" "$component" | grep -E "await|\.then|setData|setState" 2>/dev/null +``` + +Status: WIRED (call + response handling) | PARTIAL (call, no response use) | NOT_WIRED (no call) + +### Pattern: API → Database + +```bash +grep -E "prisma\.$model|db\.$model|$model\.(find|create|update|delete)" "$route" 2>/dev/null +grep -E "return.*json.*\w+|res\.json\(\w+" "$route" 2>/dev/null +``` + +Status: WIRED (query + result returned) | PARTIAL (query, static return) | NOT_WIRED (no query) + +### Pattern: Form → Handler + +```bash +grep -E "onSubmit=\{|handleSubmit" "$component" 2>/dev/null +grep -A 10 "onSubmit.*=" "$component" | grep -E "fetch|axios|mutate|dispatch" 2>/dev/null +``` + +Status: WIRED (handler + API call) | STUB (only logs/preventDefault) | NOT_WIRED (no handler) + +### Pattern: State → Render + +```bash +grep -E "useState.*$state_var|\[$state_var," "$component" 2>/dev/null +grep -E "\{.*$state_var.*\}|\{$state_var\." "$component" 2>/dev/null +``` + +Status: WIRED (state displayed) | NOT_WIRED (state exists, not rendered) diff --git a/src/install-profiles.cts b/src/install-profiles.cts index 96c18fced..9fb254387 100644 --- a/src/install-profiles.cts +++ b/src/install-profiles.cts @@ -28,6 +28,16 @@ const { readGsdCommandNames: () => string[]; }; +// #2995 (epic #1671 Phase 6.4): agent bodies join the fragment model. Markers are +// stripped at emit BEFORE any path rewrite or converter runs, so a `.claude/` -> +// `.windsurf/` regex can never reach inside a marker attribute and corrupt it — +// the same ordering #2930 established for workflows. +// eslint-disable-next-line @typescript-eslint/no-require-imports +import workflowFragmentsModule = require('./workflow-fragments.cjs'); +const { composeWorkflow: _composeWorkflow } = workflowFragmentsModule as { + composeWorkflow: (content: string, opts?: { sourcePath?: string }) => string; +}; + // --------------------------------------------------------------------------- // Profile definitions // --------------------------------------------------------------------------- @@ -360,6 +370,19 @@ function stageSkillsForProfile(srcDir: string, resolvedProfile: ResolvedProfile) * For tiered profiles, copies only agents whose full stem (e.g. 'gsd-planner') * is in resolvedProfile.agents — which is populated by resolveProfile() from * the _calls_agents_* entries in the manifest. + * + * ⚠️ RAW STAGER — ITS OUTPUT IS NOT EMISSION-READY (#2995). This stager performs a + * plain `fs.copyFileSync` and — under the default `full` profile — short-circuits + * and returns the real source directory unstaged. It does NOT strip `gsd:section` + * markers. It is still called, by `bin/install.js`'s `_stageAgents`, whose output + * feeds the inline agent loop and `installCodexConfig`; both of those compose the + * content themselves before writing, so the raw output never reaches disk. What + * changed in #2995 is that `agentsKind` and `kimiAgentsKind` no longer use it — + * they route through `stageAgentsForRuntimeWithConverter`, which composes. + * + * The invariant to preserve: anything that takes this function's output and WRITES + * it as a runtime artifact must call `composeWorkflow` on each file first, or it + * ships markers verbatim. */ function stageAgentsForProfile(srcAgentsDir: string, resolvedProfile: ResolvedProfile): string { if (resolvedProfile.skills === '*') return srcAgentsDir; @@ -834,7 +857,13 @@ function stageAgentsForRuntimeWithConverter( continue; } } - let content = fs.readFileSync(path.join(srcAgentsDir, entry.name), 'utf8'); + const agentSourcePath = path.join(srcAgentsDir, entry.name); + let content = fs.readFileSync(agentSourcePath, 'utf8'); + // #2995: strip gsd:section markers FIRST — before path rewrites, attribution, + // and the per-runtime converter. Byte-identical (no-op) for an unmarked agent; + // throws loudly naming the file for a malformed marker, never emitting a + // half-composed agent. + content = _composeWorkflow(content, { sourcePath: agentSourcePath }); if (agentCtx) { // ADR-1235 §1: pre-converter cross-cutting (matches inline loop order exactly) // Step 1: path rewrites (4 base ~/.claude/ regexes; skipped for copilot/antigravity) diff --git a/src/runtime-artifact-layout.cts b/src/runtime-artifact-layout.cts index 255c728ac..af7153f4b 100644 --- a/src/runtime-artifact-layout.cts +++ b/src/runtime-artifact-layout.cts @@ -20,7 +20,6 @@ import os from 'node:os'; import installProfiles = require('./install-profiles.cjs'); const { stageSkillsForProfile, - stageAgentsForProfile, stageAgentsForRuntimeWithConverter, stageSkillsForRuntimeAsSkills, stageCommandsForRuntimeFlat, @@ -199,7 +198,21 @@ function agentsKind(destSubpath: string, prefix: string, configDir: string): Art kind: 'agents', destSubpath, prefix, - stage: (resolved) => stageAgentsForProfile(findAgentsSourceRoot(configDir), resolved), + // #2995: a `converter: null` agents entry (claude local, zcode) previously + // staged via stageAgentsForProfile — a RAW byte copy that never reads content + // into JS, so gsd:section markers shipped verbatim. Route through the + // composing stager with an identity converter instead: same output as the raw + // copy for an unmarked agent, markers stripped for a marked one. Routing both + // agent kinds through the stager collapses what were five independent agent + // read points down to three compose call sites: this stager, bin/install.js's + // inline agent loop, and installCodexConfig's per-agent .toml writer. The + // exhaustive per-runtime sweep in tests/agent-fragments-emission.install.test.cjs + // is what keeps a fourth from appearing uncomposed. + stage: (resolved) => stageAgentsForRuntimeWithConverter( + findAgentsSourceRoot(configDir), + resolved, + (content: string) => content, + ), }; } @@ -287,7 +300,13 @@ function kimiAgentsKind(destSubpath: string, prefix: string, configDir: string): root: { yaml: string; prompt: string }; subagents: Array<{ name: string; yaml: string; prompt: string }>; }; - const stagedAgents = stageAgentsForProfile(findAgentsSourceRoot(configDir), resolved); + // #2995: compose at staging (identity converter) so the readFileSync below + // sees marker-free content — same single composing stager as agentsKind. + const stagedAgents = stageAgentsForRuntimeWithConverter( + findAgentsSourceRoot(configDir), + resolved, + (content: string) => content, + ); const subagents: Array<{ path: string; content: string }> = []; if (fs.existsSync(stagedAgents)) { for (const entry of fs.readdirSync(stagedAgents, { withFileTypes: true })) { diff --git a/tests/agent-fragments-emission.install.test.cjs b/tests/agent-fragments-emission.install.test.cjs new file mode 100644 index 000000000..092974b91 --- /dev/null +++ b/tests/agent-fragments-emission.install.test.cjs @@ -0,0 +1,227 @@ +'use strict'; + +// allow-test-rule: source-text-is-the-product see #2995 — this suite asserts on the literal bytes of +// EMITTED install artifacts, which ARE the deployed contract: a leaked `gsd:section` marker +// byte ships to every user and is loaded verbatim into a subagent's context on every dispatch. +// Same exemption basis as tests/workflow-fragments-emission.install.test.cjs (#2930). + +/** + * agent-fragments-emission.install.test.cjs — 50-test-matrix.md rows 5, 21, 22 + * (issue #2995, epic #1671 Phase 6.4). + * + * Extends `composeWorkflow`'s emission guarantee from `gsd-core/workflows/` to + * `agents/`. An engine-direct assertion is false-green for install behavior + * (ADR-1671 "Architecture and contracts"), so every row here spawns a REAL + * installer and reads what actually reached disk. + * + * ── Why this suite is ONE table-driven sweep ──────────────────────────────── + * + * Agent content is read for emission at FOUR independent points, established by + * graph analysis in .gsd/phase/chore-2995-agents-fragment-emission/40-design.md: + * + * 1. bin/install.js's inline agent loop — non-descriptor runtimes + * 2. stageAgentsForRuntimeWithConverter — the 9 descriptor runtimes + * 3. kimiAgentsKind's own readFileSync — kimi + * 4. agentsKind (converter: null) — claude(local), zcode + * + * Point 4 never reads content into JS at all: `stageAgentsForProfile` returns a + * raw byte copy — or, under the DEFAULT `full` profile, the real unstaged + * `agents/` directory itself — and `_copyStaged` copies bytes. + * + * Four parallel surfaces sharing one parser is the `DEFECT.GENERATIVE-FIX` + * class. A structural test per read point needs updating whenever a fifth + * appears, which is the update everyone forgets. So the guard is BEHAVIORAL and + * exhaustive: install every runtime at every scope that could carry agents and + * assert no marker survives. A fifth read point added without composition fails + * this test without anyone remembering to extend it. + * + * The runtime set is derived from RUNTIME_META and the capability registry at + * run time, never a hardcoded count, so a newly supported runtime cannot be + * silently under-covered. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +const { cleanup } = require('./helpers.cjs'); +const { RUNTIME_META, installerEnv, walk } = require('./helpers/install-shared.cjs'); +const { buildOverlayRepo } = require('./helpers/overlay-repo.cjs'); + +const REPO_ROOT = path.join(__dirname, '..'); + +/** The agent used as the marker probe. Any agent works — this suite proves the + * EMISSION PATH, not this file's own content. gsd-codebase-mapper is the + * smallest LARGE-tier agent, so the overlay stays cheap. */ +const PROBE_AGENT_REL = 'agents/gsd-codebase-mapper.md'; + +/** Sentinel INSIDE the marked section. Its presence proves the body survived: a + * `when="always"` section must be kept, not dropped. Without this, an emission + * path that dropped the whole section would pass a marker-absence check + * vacuously. */ +const BODY_SENTINEL = 'GSD2995 probe body retained sentinel'; +const MARKER_TOKEN = 'gsd:section'; + +/** Path fragment identifying artifacts derived from the probe agent. Emitted + * artifacts are named after the source agent on every runtime that emits them + * (`.md`, Codex's `.toml`, Kimi's `subagents/.{yaml,md}`), so this is the + * precise derived-artifact filter. + * + * Scoping matters: several SHIPPED library files under `gsd-core/bin/lib/` + * (workflow-fragments.cjs, section-manifest.cjs, install-profiles.cjs, + * runtime-artifact-layout.cjs) legitimately contain the literal token in their + * own source — they implement the grammar. An unscoped tree sweep flags those + * and can never pass. */ +const PROBE_STEM = 'gsd-codebase-mapper'; + +/** Real file + one `when="always"` section wrapping an injected sentinel. + * `always` is deliberate: it is the one atom whose predicate is + * unconditionally true, so a dropped body is unambiguously a defect rather + * than correct gating. */ +function markedProbeAgent() { + const original = fs.readFileSync(path.join(REPO_ROOT, PROBE_AGENT_REL), 'utf8'); + return original + [ + '', + '', + '', + BODY_SENTINEL, + '', + '', + '', + ].join('\n'); +} + +/** Scopes worth installing for one runtime: always `global` (the inline + * agent loop is not descriptor-declared), plus every scope whose descriptor + * declares an agent-bearing kind. Derived at run time from the registry — + * `claude` declares its raw `agents` kind ONLY at local scope, so a + * global-only sweep would miss the primary runtime's raw path. */ +function scopesForRuntime(registry, runtime) { + const scopes = new Set(['global']); + const layout = registry?.runtimes?.[runtime]?.runtime?.artifactLayout; + if (layout) { + for (const scope of ['global', 'local']) { + for (const entry of layout[scope] || []) { + if (entry.kind === 'agents' || entry.kind === 'kimi-agents') scopes.add(scope); + } + } + } + return [...scopes]; +} + +/** Spawn a real install of one runtime at one scope against `repoRoot`. */ +function spawnInstall(repoRoot, runtime, scope) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), `gsd-2995-${runtime}-${scope}-`)); + const args = [ + '--preserve-symlinks', + '--preserve-symlinks-main', + path.join(repoRoot, 'bin', 'install.js'), + `--${runtime}`, + ]; + const cwd = root; + if (scope === 'global') args.push('--global', '--config-dir', root); + else args.push('--local'); + const result = spawnSync(process.execPath, args, { + cwd, + encoding: 'utf8', + env: installerEnv({ HOME: root, USERPROFILE: root }), + }); + return { result, root }; +} + +/** Emitted files under `root` that are derived from the probe agent AND contain + * `token`. The `derivedOnly` filter is what keeps shipped library sources — + * which legitimately carry the marker grammar — out of the leak set. */ +function filesContaining(root, token, derivedOnly = true) { + if (!fs.existsSync(root)) return []; + const hits = []; + for (const abs of walk(root)) { + const rel = path.relative(root, abs).replace(/\\/g, '/'); + if (derivedOnly && !rel.includes(PROBE_STEM)) continue; + let buf; + try { + buf = fs.readFileSync(abs); + } catch { + continue; // unreadable entry (socket, dangling link) is not a leak + } + if (buf.includes(token)) hits.push(rel); + } + return hits; +} + +/** Run the whole matrix against one repo root. */ +function collectLeaks(repoRoot, t) { + const registry = require('../gsd-core/bin/lib/capability-registry.cjs'); + const leaks = []; + const retained = []; + const failures = []; + for (const runtime of Object.keys(RUNTIME_META)) { + for (const scope of scopesForRuntime(registry, runtime)) { + const { result, root } = spawnInstall(repoRoot, runtime, scope); + t.after(() => cleanup(root)); + if (result.status !== 0) { + failures.push(`${runtime}/${scope} exit ${result.status}: ${String(result.stderr || '').slice(-300)}`); + continue; + } + const markerHits = filesContaining(root, MARKER_TOKEN); + if (markerHits.length > 0) leaks.push(`${runtime}/${scope} -> ${markerHits.join(', ')}`); + if (filesContaining(root, BODY_SENTINEL).length > 0) retained.push(`${runtime}/${scope}`); + } + } + return { leaks, retained, failures }; +} + +// ─── Row 5: no runtime emits a surviving agent section marker ───────────────── + +test('row 5 — no runtime emits an agent gsd:section marker at any agent-bearing scope', (t) => { + const overlay = buildOverlayRepo({ [PROBE_AGENT_REL]: markedProbeAgent() }); + t.after(() => cleanup(overlay)); + + const { leaks, retained, failures } = collectLeaks(overlay, t); + + assert.deepStrictEqual( + failures, + [], + `every install in the sweep must succeed, else the sweep proves nothing:\n${failures.join('\n')}`, + ); + assert.deepStrictEqual( + leaks, + [], + `every emission path must strip agent section markers before writing:\n${leaks.join('\n')}`, + ); + + // Anti-vacuity: marker absence alone is satisfiable by dropping the section + // entirely. A `when="always"` body must be KEPT, so at least one runtime must + // show the sentinel. + assert.ok( + retained.length > 0, + 'no runtime retained the always-section body — marker absence is being satisfied by ' + + 'dropping content, not by stripping markers', + ); +}); + +// ─── Rows 21/22: the negative control — identity composer MUST leak ─────────── + +test('rows 21/22 — an identity composer makes agent markers leak', (t) => { + const overlay = buildOverlayRepo({ + [PROBE_AGENT_REL]: markedProbeAgent(), + 'gsd-core/bin/lib/workflow-fragments.cjs': 'module.exports = { composeWorkflow: (c) => c };\n', + }); + t.after(() => cleanup(overlay)); + + const { leaks, failures } = collectLeaks(overlay, t); + + assert.deepStrictEqual( + failures, + [], + `identity-stub installs must still succeed:\n${failures.join('\n')}`, + ); + assert.ok( + leaks.length > 0, + 'with composeWorkflow stubbed to identity, agent markers MUST reach disk. They did not, ' + + 'so row 5 proves nothing — it passes for some reason other than the composer working.', + ); +}); diff --git a/tests/agent-marker-documentation-guard.test.cjs b/tests/agent-marker-documentation-guard.test.cjs new file mode 100644 index 000000000..3710924ff --- /dev/null +++ b/tests/agent-marker-documentation-guard.test.cjs @@ -0,0 +1,148 @@ +'use strict'; + +// allow-test-rule: source-text-is-the-product see #2995 — parses the literal text of shipped +// agent .md files, which IS the deployed contract that composeWorkflow consumes at install time. + +/** + * agent-marker-documentation-guard.test.cjs — 50-test-matrix.md rows 11 and 12 + * (issue #2995, epic #1671 Phase 6.4). + * + * #2930 scoped `composeWorkflow` to `gsd-core/workflows/` for one specific + * reason: a document that merely DOCUMENTS the marker syntax with an UNFENCED + * example is indistinguishable from a real marker, so the composer would treat + * it as one and drop that line from the emitted artifact. `docs/reference/ + * workflow-fragments.md` was named as the live instance of that class. + * + * #2995 widens the composed scope to `agents/`, which makes the class reachable + * for agent files for the first time. These two rows are the guard. + */ + +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); + +const { parseWorkflowSections, composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs'); + +const AGENTS_DIR = path.join(__dirname, '..', 'agents'); + +// ─── Row 11: a FENCED marker example is literal, not a marker ───────────────── + +test('row 11 — a fenced marker example in an agent survives composition verbatim', () => { + const doc = [ + '---', + 'name: gsd-example', + '---', + '', + '# Example agent', + '', + 'To gate a section, write:', + '', + '```markdown', + '', + 'body', + '', + '```', + '', + 'That is the whole grammar.', + '', + ].join('\n'); + + const sections = parseWorkflowSections(doc, 'agents/gsd-example.md'); + assert.deepStrictEqual( + sections.filter((s) => s.explicit).map((s) => s.id), + [], + 'a fenced example must produce NO explicit section — it is documentation, not a marker', + ); + + const composed = composeWorkflow(doc, { sourcePath: 'agents/gsd-example.md' }); + assert.equal( + composed, + doc, + 'a document whose only marker-shaped lines are fenced must compose byte-identically', + ); +}); + +// ─── Row 12: no shipped agent carries a marker-shaped line outside a fence ──── +// +// This is the guard that makes row 11's protection load-bearing. If someone adds +// an UNFENCED marker example to an agent as documentation, the composer parses it +// as a real marker and silently swallows the line at emit. Parsing every shipped +// agent and requiring zero EXPLICIT sections catches that at test time instead of +// in a user's installed tree. +// +// It is deliberately an equality-to-empty assertion rather than a count: when the +// per-agent manifest family that would make agent gating admissible eventually +// lands, this test must be revisited on purpose, not silently satisfied. + +test('row 12 — no shipped agent carries an unfenced gsd:section marker', () => { + const offenders = []; + for (const name of fs.readdirSync(AGENTS_DIR).sort()) { + if (!name.endsWith('.md')) continue; + const rel = path.posix.join('agents', name); + const content = fs.readFileSync(path.join(AGENTS_DIR, name), 'utf8'); + const explicit = parseWorkflowSections(content, rel).filter((s) => s.explicit); + if (explicit.length > 0) offenders.push(`${rel}: ${explicit.map((s) => s.id).join(', ')}`); + } + + assert.deepStrictEqual( + offenders, + [], + 'An agent carries a gsd:section marker outside a fence. If it is DOCUMENTATION, fence it — ' + + 'the composer cannot tell an unfenced example from a real marker and will drop the line ' + + 'from the emitted agent. If it is meant as real gating, it will not work: ' + + 'gen-section-manifest.cjs scans only gsd-core/workflows/, so an agent atom has no consumer ' + + 'and evaluates false forever (see ADR-1671, "the grammar does NOT extend to agents/").\n' + + `Offenders:\n ${offenders.join('\n ')}`, + ); +}); + +// ─── Row 12b: the guard is not vacuous — a whole-line unfenced marker IS a marker ── +// +// Refined by a real failure: the grammar is WHOLE-LINE only. An open marker placed +// INLINE inside a sentence is NOT recognised as an open — so the hazard row 12 +// guards against is specifically an unfenced marker on its OWN line, which is +// exactly how a documentation example is normally written. + +test('row 12b — a whole-line unfenced marker in agent prose is detected as a real marker', () => { + const doc = [ + '---', + 'name: gsd-example', + '---', + '', + 'To open a section, write:', + '', + '', + 'body', + '', + '', + ].join('\n'); + + const explicit = parseWorkflowSections(doc, 'agents/gsd-example.md').filter((s) => s.explicit); + assert.deepStrictEqual( + explicit.map((s) => s.id), + ['demo'], + 'a whole-line UNFENCED marker must parse as a real marker — this is precisely why row 12 ' + + 'exists; if this assertion ever fails, row 12 is guarding against nothing', + ); +}); + +// ─── Row 12c: an INLINE marker-shaped span is not a marker ─────────────────── + +test('row 12c — an inline marker-shaped span mid-sentence is not treated as an open marker', () => { + const doc = [ + '---', + 'name: gsd-example', + '---', + '', + 'Write to open a section.', + '', + ].join('\n'); + + const explicit = parseWorkflowSections(doc, 'agents/gsd-example.md').filter((s) => s.explicit); + assert.deepStrictEqual( + explicit.map((s) => s.id), + [], + 'an inline marker-shaped span is prose, not a marker — the grammar is whole-line only', + ); +}); diff --git a/tests/fixtures/install-tree/antigravity.json b/tests/fixtures/install-tree/antigravity.json index 8a11c4dad..7f34afcaf 100644 --- a/tests/fixtures/install-tree/antigravity.json +++ b/tests/fixtures/install-tree/antigravity.json @@ -69,6 +69,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -161,6 +162,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/augment.json b/tests/fixtures/install-tree/augment.json index 76c65a755..a2aa49e83 100644 --- a/tests/fixtures/install-tree/augment.json +++ b/tests/fixtures/install-tree/augment.json @@ -140,6 +140,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -232,6 +233,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/claude-local.json b/tests/fixtures/install-tree/claude-local.json index f6b88c148..a8625f149 100644 --- a/tests/fixtures/install-tree/claude-local.json +++ b/tests/fixtures/install-tree/claude-local.json @@ -139,6 +139,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -231,6 +232,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/claude.json b/tests/fixtures/install-tree/claude.json index 4b7006052..57eb7b36d 100644 --- a/tests/fixtures/install-tree/claude.json +++ b/tests/fixtures/install-tree/claude.json @@ -68,6 +68,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -160,6 +161,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/cline.json b/tests/fixtures/install-tree/cline.json index 6f515c197..e9b94fd3c 100644 --- a/tests/fixtures/install-tree/cline.json +++ b/tests/fixtures/install-tree/cline.json @@ -72,6 +72,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -164,6 +165,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/codebuddy.json b/tests/fixtures/install-tree/codebuddy.json index fbbce94c1..b277992ce 100644 --- a/tests/fixtures/install-tree/codebuddy.json +++ b/tests/fixtures/install-tree/codebuddy.json @@ -140,6 +140,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -232,6 +233,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/codex.json b/tests/fixtures/install-tree/codex.json index d0dacfe47..3ed7a3eee 100644 --- a/tests/fixtures/install-tree/codex.json +++ b/tests/fixtures/install-tree/codex.json @@ -175,6 +175,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -267,6 +268,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/copilot.json b/tests/fixtures/install-tree/copilot.json index 436a174d6..768e456be 100644 --- a/tests/fixtures/install-tree/copilot.json +++ b/tests/fixtures/install-tree/copilot.json @@ -70,6 +70,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -162,6 +163,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/cursor.json b/tests/fixtures/install-tree/cursor.json index 452a6161d..6a33082d8 100644 --- a/tests/fixtures/install-tree/cursor.json +++ b/tests/fixtures/install-tree/cursor.json @@ -69,6 +69,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -161,6 +162,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/hermes.json b/tests/fixtures/install-tree/hermes.json index 48b2b1656..dac9706f6 100644 --- a/tests/fixtures/install-tree/hermes.json +++ b/tests/fixtures/install-tree/hermes.json @@ -69,6 +69,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -161,6 +162,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/kilo.json b/tests/fixtures/install-tree/kilo.json index ad91d888b..0e50fa3a5 100644 --- a/tests/fixtures/install-tree/kilo.json +++ b/tests/fixtures/install-tree/kilo.json @@ -140,6 +140,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -232,6 +233,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/kimi-code.json b/tests/fixtures/install-tree/kimi-code.json index d7f3c0427..bab4816f8 100644 --- a/tests/fixtures/install-tree/kimi-code.json +++ b/tests/fixtures/install-tree/kimi-code.json @@ -99,6 +99,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -191,6 +192,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/kimi.json b/tests/fixtures/install-tree/kimi.json index 8744125f4..261eee08b 100644 --- a/tests/fixtures/install-tree/kimi.json +++ b/tests/fixtures/install-tree/kimi.json @@ -135,6 +135,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -227,6 +228,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/opencode.json b/tests/fixtures/install-tree/opencode.json index 472eff44e..2a438c20f 100644 --- a/tests/fixtures/install-tree/opencode.json +++ b/tests/fixtures/install-tree/opencode.json @@ -140,6 +140,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -232,6 +233,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/pi.json b/tests/fixtures/install-tree/pi.json index e701f615b..22d5d6c3f 100644 --- a/tests/fixtures/install-tree/pi.json +++ b/tests/fixtures/install-tree/pi.json @@ -37,6 +37,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -129,6 +130,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/qwen.json b/tests/fixtures/install-tree/qwen.json index 31b2a1476..ff1fc0f38 100644 --- a/tests/fixtures/install-tree/qwen.json +++ b/tests/fixtures/install-tree/qwen.json @@ -69,6 +69,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -161,6 +162,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/trae.json b/tests/fixtures/install-tree/trae.json index 8dd6e8d6d..995f0ac5c 100644 --- a/tests/fixtures/install-tree/trae.json +++ b/tests/fixtures/install-tree/trae.json @@ -69,6 +69,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -161,6 +162,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/windsurf.json b/tests/fixtures/install-tree/windsurf.json index 3408f3f3f..5093184ec 100644 --- a/tests/fixtures/install-tree/windsurf.json +++ b/tests/fixtures/install-tree/windsurf.json @@ -69,6 +69,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -161,6 +162,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md", diff --git a/tests/fixtures/install-tree/zcode.json b/tests/fixtures/install-tree/zcode.json index 0a25e9dff..5dda4b917 100644 --- a/tests/fixtures/install-tree/zcode.json +++ b/tests/fixtures/install-tree/zcode.json @@ -140,6 +140,7 @@ "gsd-core/references/debugger-repro-hardening.md", "gsd-core/references/debugger-sbfl.md", "gsd-core/references/debugger-semantic-recall.md", + "gsd-core/references/debugger-techniques.md", "gsd-core/references/decimal-phase-calculation.md", "gsd-core/references/doc-conflict-engine.md", "gsd-core/references/domain-probes.md", @@ -232,6 +233,7 @@ "gsd-core/references/user-story-template.md", "gsd-core/references/verification-overrides.md", "gsd-core/references/verification-patterns.md", + "gsd-core/references/verifier-wiring-patterns.md", "gsd-core/references/verify-mvp-mode.md", "gsd-core/references/workstream-flag.md", "gsd-core/references/worktree-branch-check.md",