diff --git a/docs/adr/1671-dynamic-context-management-platform.md b/docs/adr/1671-dynamic-context-management-platform.md new file mode 100644 index 000000000..58ddfc13b --- /dev/null +++ b/docs/adr/1671-dynamic-context-management-platform.md @@ -0,0 +1,125 @@ +# ADR-1671: Dynamic context management platform + +- **Status:** Proposed +- **Date:** 2026-06-24 +- **Extends:** ADR-0002 (Command Contract Validation Module), ADR-457 (build-at-publish generation model for `bin/lib/*.cjs`) +- **Relates:** ADR-857 §7 (Connected-Capability / MCP contract — kept deferred by this ADR) + +## Context + +GSD ships command and workflow content as large, hand-edited Markdown files. Two structural problems compound: + +1. **Authoring is monolithic.** A single workflow body carries every branch inline. `gsd-core/workflows/plan-phase.md` is 93,973 bytes / 1,770 lines; `execute-phase.md` is 93,426 bytes. Mutually-exclusive paths (`--prd`, `--ingest`, `--mvp`, `--reviews`) all live in the same file, so a runtime loads guidance for branches a given invocation will never take. + +2. **One payload ships to every runtime.** Install copies the whole `gsd-core/` tree (3.4 MB, 89 workflows, 1.7 MB) **byte-identical to all 15 runtimes** via `copyWithPathReplacement` (`bin/install.js`). The only per-runtime work is string rewrites and description truncation. There is **no per-runtime trimming or splitting**. + +The result is constant pressure against size caps, enforced today only against *source* files (not emitted output) by a two-part guard (issue #1074): a per-file baseline ratchet plus per-tier hard caps (workflows XL 96 KiB / LARGE 60 KiB / DEFAULT 40 KiB; agents XL 56 KiB / LARGE 48 KiB / DEFAULT 24 KiB). Several files have almost no headroom — `agents/gsd-verifier.md` has **293 bytes**. The one true emission-time cap, Windsurf's 12,000-byte limit (`src/runtime-artifact-conversion.cts`), is a hard `throw` with no graceful fallback. Adding one rule to a tight file forces an extract-to-`references/` refactor (`DEFECT.AGENT-FILE-SIZE-CAP-BREACH`), turning a one-line edit into a multi-file change that ripples across stub frontmatter, the workflow body, reference fragments, and `docs/` — each guarded by a different lint. + +A separate but related pain: the repo-root `CONTEXT.md` predicate fact-store (~935 lines, ~200 KB of `CLASS.subkey=value` predicates that agent briefs are required to "cite verbatim") has **no programmatic reader, validator, or selector**. Briefs are hand-assembled, and `META.RULE.brief-must-cite-doc` is enforced only socially — paraphrasing from memory has caused real violations (5/8 agents in one documented batch). + +### The machinery already exists, in silos + +Research into the codebase found that most JIT primitives are already present and proven; they are just single-purpose and not composed: + +- **Lazy reference loading** — the init bundle. `gsd_run query init.` (`src/init.cts`) returns JSON of *paths + flags, not contents*; the model reads only the files it needs ("paths only to minimize orchestrator context", `plan-phase.md:66`). This is Anthropic's recommended "lightweight identifiers over payloads" pattern, in production. +- **Progressive disclosure** — `gsd-core/workflows/help.md` reads only the one mode file matching the argument (`brief` 0.9 KB / `default` 1.9 KB / `full` 34 KB). +- **Token-budgeted assembly** — `src/prompt-budget.cts` `applyBudget()` already does priority-ordered, budget-trimmed composition with an omission note — but it is walled into the cross-AI review pipeline only. +- **Pointer-passing channel** — `src/io.cts` spills any payload > 50 KB to a tmpfile and returns `@file:`. +- **A codegen factory + drift-guard harness** — 13 generators share one `--check`/`--write` idiom (derive fresh, diff committed, exit 1 on drift). `scripts/gen-plugin-skills.cjs` already generates 69 shipped `SKILL.md` files from `commands/gsd/*.md`. +- **A reusable structured-markdown parser** — `src/markdown-sectionizer.cts`, already powering the per-phase `` fact-store reader (`src/decisions.cts`). + +### External practice + +The closest external analogs are Anthropic Agent Skills' three-tier progressive disclosure (metadata → `SKILL.md` → bundled references), MCP resources/prompts/deferred-tools (list-then-fetch JIT), and priority/token-budget prompt renderers (Priompt, VS Code `@vscode/prompt-tsx`) that include the highest-priority fragments that fit a budget via a binary-search cutoff, with `flexReserve` floors for load-bearing content and `` for a stable cacheable prefix. The portability catch is real and load-bearing: only the Skills *format* (directory + `SKILL.md` + frontmatter) is an open standard; native lazy loading is Claude-specific, and GSD's 15 runtimes do not all support skills or MCP (cf. surface-mismatch bugs #1614 antigravity, #1615 windsurf). + +## Decision + +Adopt a **dynamic context management platform** built on a hybrid of build-time and run-time assembly, reusing the existing seams rather than inventing new infrastructure: + +1. **Fragment store (authoring model).** Author workflow content as composable, priority-tagged fragments (workflow sections + shared `references/` + predicate-derived blocks), each carrying an applicability condition (which flags / capabilities / runtimes require it). This is the net-new authoring discipline. + +2. **Build-time composer + per-runtime budget emission (the universal floor).** Generalize `prompt-budget.cts` out of the review silo into a shared `context-composer` seam (`src/*.cts` → `build:lib` → `bin/lib/*.cjs`). At build/install time, for each command × runtime, the composer selects the needed fragments and trims by priority to fit that runtime's measured cap (`scripts/workflow-size.cjs` `lfByteCount`), emitting a right-sized artifact through the existing converter. Caps move from *source* to *emitted output*; the Windsurf 12 KB `throw` becomes a graceful auto-trim/auto-extract. This is what makes caps stop biting on non-lazy runtimes, and it requires no runtime feature — so it is the universal floor. + +3. **Progressive disclosure where the host supports it.** On lazy-loading hosts (Claude Code and the Agent SDK), keep the stub + `@-ref` model and let the init bundle name exactly which files to read; the body and references load on demand. + +4. **Run-time selection via the init seam (per-request precision).** Extend the init bundle / `command-routing-hub` dispatch (`src/command-routing-hub.cts`) to emit a typed manifest of which sections / references / predicates a *specific* invocation needs (given parsed args, flags, phase state, active capabilities), reusing the `@file:` spill channel for assembled fragments. This is layered on top of the fragment store. + +5. **Formalize the `CONTEXT.md` predicate fact-store → JIT selector.** Give the predicate grammar a parser (on `markdown-sectionizer`), an ID-uniqueness validator, a `--check`/`--write` drift-guard, and a `task → relevant predicate set` selector. This converts hand-assembled briefs into JIT-generated context and attacks the maintainer-side "edit a 200 KB file by hand" pain directly. **This is sequenced first** (see Prototype) because it is the smallest, lowest-risk piece that proves the whole pattern. + +6. **Defer MCP (Connected-Capability).** Per ADR-857 §7 / #956, a served MCP catalog (resources/prompts/deferred-tools) remains an additive future enhancement for MCP-capable runtimes — never a replacement for the file-copy floor. Not in scope here. + +### Options considered + +| Option | Summary | Fixes caps? | Runtime compat | Decision | +|---|---|---|---|---| +| A. Progressive-disclosure authoring | Metadata-first files + one-level references; lean on host lazy-load | Partial; needs host lazy-load | Authoring universal; native JIT Claude-first | Adopt as a layer | +| B. Build-time composer + per-runtime budget emission | Composer trims fragments to each runtime cap, emits right-sized files | Yes — measured before write | Universal floor | **Adopt as core** | +| C. Run-time selection via init seam | Init bundle names which slices this invocation needs | Reduces per-invocation context | Broad (the `gsd_run` shim is universal) | Adopt after B | +| D. MCP served catalog | Serve content as resources/prompts/deferred-tools | For MCP hosts only | Partial; needs 2nd channel | Defer (ADR-857 §7) | +| E. Predicate fact-store → JIT selector | Parse/validate/select `CONTEXT.md` predicates | Maintainer-side big-file pain | N/A (build + orchestrator) | **Adopt first** | + +Pure Agent Skills (A alone) and pure MCP (D alone) were rejected as the foundation because both are runtime-partial; only build-time emission (B) relieves caps on every runtime. + +## Architecture and contracts + +- **Fragment unit (open question, see below):** either separate files (clean lazy-load + INVENTORY rows) or in-file section markers (``, mirroring the existing `` markers consumed by `scripts/gen-loop-host-contract.cjs`). +- **Composer contract:** priority + binary-search cutoff to a per-runtime budget; `flexReserve`-style floors for load-bearing fragments (`META.RULE` citation rules, contribution gates, closing-keyword rules); a byte-stable canonical prefix (``) kept identical across runtimes to preserve KV-cache warmth and keep launcher-parity tests green. +- **Budget unit:** bytes for emission caps (matches `lfByteCount`, deterministic, offline-safe); a token estimate for run-time selection. +- **Determinism + drift-guard:** every generated artifact follows the universal `--check`/`--write` idiom and is committed; any constant shared between two surfaces gets a `DEFECT.GENERATIVE-FIX` parity assertion. Caps are asserted on **emitted per-runtime bytes** via real spawn-install tests (engine-direct tests are false-green for install behavior). +- **Boundary coverage:** the composer's budget logic is tested at `cap-1 / cap / cap+1` per `RULESET.TESTS.boundary-coverage`. + +## Migration path + +Sequenced to de-risk — prove the pattern on the smallest surface first, scale last: + +1. **This ADR** establishes the platform, the fragment/composer contract, emission-time caps, and the drift-guard requirement. +2. **Prototype the predicate fact-store (Option E)** — *landed with this ADR as a non-shipping reference example* under `examples/dynamic-context-management/` (see Prototype below). +3. **Lift `prompt-budget.cts`** out of the review silo into a shared `context-composer` seam with fast-check property tests + boundary coverage. +4. **Pilot fragmentization on one XL workflow** (`plan-phase.md` or `execute-phase.md`): split into priority-tagged sections + applicability; composer emits per-runtime; prove byte-identical-or-smaller output and green `gsd-test` docker. +5. **Move caps from source to emitted output**; turn the Windsurf `throw` into graceful auto-trim; auto-regenerate size baselines on intentional edits. +6. **Wire the init bundle (C)** to emit a per-invocation sections manifest; workflows consume it. +7. **Roll out across LARGE/XL tiers**; update INVENTORY families + parity tests. +8. **(Deferred)** MCP served catalog (ADR-857 §7 / #956). + +**Ordering landmine:** any generator consuming compiled output must run *after* `build:lib` (tsc), like `gen-plugin-skills` / `gen-capability-registry`; regenerating before `build:lib` silently drops unbuilt modules (`gsd-inventory-manifest-regen-needs-build`). + +## Consequences + +**Positive** +- Caps stop biting: each runtime's emitted artifact is measured and trimmed before write. +- A discovered fact lands in one fragment / predicate, not 4 hand-edited surfaces. +- Reuses the existing converter, drift-guard, boundary-test, and `markdown-sectionizer` infrastructure — the net-new pieces are only the fragment model and the composer. +- Opens a path to collapse the 10+ hand-written per-runtime body converters toward a data-driven spec. + +**Negative / risks** +- Trimming a load-bearing fragment is a correctness hazard (history: paraphrased `META.RULE` → agent violations). Mitigate with `flexReserve` floors, a Promptfoo-style eval gate, and boundary tests. +- Per-runtime emission multiplies artifacts across the 15 × N matrix (inventory/parity surface). +- Build-order fragility (must run after `build:lib`). +- Dual-surface drift if any future MCP channel is added — requires parity assertions. + +## Prototype (step 2, Option E) — non-shipping reference example + +A working prototype proves the platform pattern end-to-end. It ships as a **reference example only**, under `examples/dynamic-context-management/` — deliberately outside the build (`src/` → `bin/lib/`), the npm package `files[]`, the installer, and the CI test suite (`tests/`). Nothing in it is compiled into or installed with GSD; the production implementation lands in a later phase. + +- `examples/dynamic-context-management/context-predicates.cjs` — pure parser/selector: `parsePredicates(markdown)` (handles bare and list-item backtick predicate forms, splits on first `=`, skips fenced code / blockquote prose, detects duplicate IDs), `selectPredicates(predicates, {klass, prefix, contains})` (the JIT "task → predicate set" selector), and `buildIndex(predicates)` (deterministic, sorted). +- `examples/dynamic-context-management/gen-context-index.cjs` — self-contained CLI with `--check`/`--write` drift-guard plus a `--select ` mode demonstrating JIT brief assembly. +- `examples/dynamic-context-management/CONTEXT-INDEX.json` — sample generated index: **393 predicates, 18 classes**. +- `examples/dynamic-context-management/demo.cjs` + `README.md` — runnable usage example and notes. + +During research the slice was validated with 42 behavioral tests (predicate forms, fenced-code / prose skipping, duplicate-id detection, the selector, a deterministic index, and a fast-check property test); those return as CI tests under `tests/` with the production implementation. + +The prototype immediately surfaced **3 latent duplicate predicate IDs** in `CONTEXT.md` (`RULESET.WORKFLOW_MARKDOWN.FENCES`, `RULESET.GEMINI.TOOLS.ask_user`, `RULESET.GEMINI.TEST_SENTINEL`) — integrity drift no existing tool catches. Production `--check` can be made to fail on *new* duplicates once the existing three are reconciled. + +Prototype scope notes: the parser is intentionally self-contained for the example; production should consume the compiled `markdown-sectionizer` seam, live under `src/` → `bin/lib/`, and be drift-guarded by a generator wired into the build **after** `build:lib`. + +## Open questions + +1. Fragment unit: separate files vs in-file section markers? +2. Build-time emission vs run-time assembly as the primary surface during migration (double-write vs per-workflow cutover)? +3. Whether/when to invest in per-runtime native channels (skills, MCP) above the universal file floor. + +## Related + +- ADR-0002 — Command Contract Validation Module (the stub `` @-ref contract this platform's emission must keep satisfying). +- ADR-457 — build-at-publish generation model (the codegen + drift-guard precedent the composer extends). +- ADR-857 §7 — Connected-Capability / MCP contract (the deferred served-catalog channel). diff --git a/examples/dynamic-context-management/CONTEXT-INDEX.json b/examples/dynamic-context-management/CONTEXT-INDEX.json new file mode 100644 index 000000000..82028610d --- /dev/null +++ b/examples/dynamic-context-management/CONTEXT-INDEX.json @@ -0,0 +1,2407 @@ +{ + "schemaVersion": 1, + "count": 393, + "classes": { + "ARCH": 1, + "CI": 2, + "CONFIG": 1, + "DEFECT": 161, + "EXEC": 8, + "GSD-RESEARCH": 6, + "LEARNING": 1, + "META": 4, + "PLANNING": 3, + "PR": 2, + "PRED": 68, + "PROC": 14, + "RELEASE-NOTES": 31, + "RULESET": 59, + "SESSION": 9, + "WAVE": 5, + "WORKSTREAM": 5, + "WORKTREE": 13 + }, + "predicates": [ + { + "id": "ARCH.SKILL.improve-codebase.next-candidates", + "klass": "ARCH", + "value": "[Workstream Name Policy Module, Workstream Progress Projection Module, Active Workstream Pointer Store Module]", + "line": 448 + }, + { + "id": "CI.GATE.changeset-lint", + "klass": "CI", + "value": "hard-fail for user-facing code diffs unless .changeset/* or PR has no-changelog label", + "line": 432 + }, + { + "id": "CI.GATE.issue-link-required", + "klass": "CI", + "value": "hard-fail if PR body lacks closes/fixes/resolves #", + "line": 431 + }, + { + "id": "CONFIG.SEAM.loadConfig-context", + "klass": "CONFIG", + "value": "loadConfig(cwd,{workstream}) replaces env-mutation fallback; no temporary process.env GSD_WORKSTREAM rewrites", + "line": 463 + }, + { + "id": "DEFECT.AGENT-FILE-SIZE-CAP-BREACH.detect", + "klass": "DEFECT", + "value": "tests/planner-decomposition.test.cjs (\"planner is under 45K chars (proves mode sections were extracted)\") and tests/reachability-check.test.cjs (\"file stays under 50000 char limit\")", + "line": 665 + }, + { + "id": "DEFECT.AGENT-FILE-SIZE-CAP-BREACH.fix-forward", + "klass": "DEFECT", + "value": "mirror MVP mode pattern — extract full rules to gsd-core/references/planner-.md, leave a slim Detection section in the agent file with @-reference to the new file", + "line": 666 + }, + { + "id": "DEFECT.AGENT-FILE-SIZE-CAP-BREACH.state", + "klass": "DEFECT", + "value": "gsd-planner.md is already 49,121 chars on main (over 45K); test fails on main; net-new content makes it strictly worse", + "line": 664 + }, + { + "id": "DEFECT.AGENT-FILE-SIZE-CAP-BREACH.symptom", + "klass": "DEFECT", + "value": "adding to agents/gsd-planner.md (or other large agent files) exceeds the 45K char extraction-evidence threshold", + "line": 663 + }, + { + "id": "DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.detect", + "klass": "DEFECT", + "value": "tests/bug-2543-gsd-slash-namespace.test.cjs prints \"Found N retired /gsd- reference(s) — use /gsd: instead\" with line-number-precise violations", + "line": 864 + }, + { + "id": "DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.examples", + "klass": "DEFECT", + "value": "#3541 implementation included a typical /gsd-update path comment in installer-migration-report.cjs; caught by tests/bug-2543-gsd-slash-namespace.test.cjs (#3443 invariant)", + "line": 863 + }, + { + "id": "DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.fix-forward", + "klass": "DEFECT", + "value": "replace /gsd- with /gsd: at the cited file:line; healthy emergent property — project-wide invariant test catches drift agents would never self-correct", + "line": 865 + }, + { + "id": "DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.lesson", + "klass": "DEFECT", + "value": "agent-trust-but-verify is load-bearing — sub-agent reporting \"done\" is not a substitute for running the full suite; the invariant test surfaces drift even in doc-only changes", + "line": 866 + }, + { + "id": "DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT.symptom", + "klass": "DEFECT", + "value": "sub-agent writes /gsd- (legacy hyphen syntax) in code comments or doc strings while implementing a fix; lands as part of the implementation diff", + "line": 862 + }, + { + "id": "DEFECT.BOT-BRANCH-STALE-BASE.detect", + "klass": "DEFECT", + "value": "git merge-base origin/ origin/main returns the bot branch tip — confirms the bot branch is an ancestor of main, just stale", + "line": 645 + }, + { + "id": "DEFECT.BOT-BRANCH-STALE-BASE.examples", + "klass": "DEFECT", + "value": "#3309 fix/3309-checkpoint-type-human-verify-burns-token (was at e14ef535; main at 2e87c60a)", + "line": 644 + }, + { + "id": "DEFECT.BOT-BRANCH-STALE-BASE.fix-forward", + "klass": "DEFECT", + "value": "git checkout --detach origin/main; do work; git checkout -b ; force-push with --force-with-lease", + "line": 646 + }, + { + "id": "DEFECT.BOT-BRANCH-STALE-BASE.symptom", + "klass": "DEFECT", + "value": "auto-branch.yml creates fix/{N}-{slug} when issue is filed; branch is anchored to issue-creation main; by the time work begins, main has moved", + "line": 643 + }, + { + "id": "DEFECT.CANARY-VERSION-LEAK.detect", + "klass": "DEFECT", + "value": "jq -r .version package.json on origin/main shows a -canary suffix; OR npm view dist-tags shows latest != main's version", + "line": 819 + }, + { + "id": "DEFECT.CANARY-VERSION-LEAK.examples", + "klass": "DEFECT", + "value": "2026-05-16 audit found origin/main + origin/feat/3575-enforcement-hardening both at \"version\": \"1.50.0-canary.0\" in sdk/package.json AND root package.json; npm view @opengsd/gsd-sdk versions returned [\"0.1.0\"] only, dist-tag latest=0.1.0, @1.50.0-canary.0 404 — confirms the string is metadata-only, never published. git log -S '\"version\": \"1.50.0-canary.0\"' origin/main blamed commit 2d32ad82 fix(plan-phase)... (#3206), a fix PR that accidentally carried the version bump from a dev-branch base", + "line": 818 + }, + { + "id": "DEFECT.CANARY-VERSION-LEAK.fix-forward", + "klass": "DEFECT", + "value": "open a chore/* PR against main that resets the version strings to the canonical pre-canary stable; rebase open PRs to pick it up; gate at PR open with a CI check that rejects -canary versions on PRs targeting main", + "line": 820 + }, + { + "id": "DEFECT.CANARY-VERSION-LEAK.symptom", + "klass": "DEFECT", + "value": "package.json version on main carries a -canary. suffix that per release policy belongs to the dev branch only; nothing publishable depends on the version string at runtime, but every consumer of the version metadata (release flow, install banners, statusline) sees the dev-channel label", + "line": 817 + }, + { + "id": "DEFECT.CHANGESET-PR-FIELD-DRIFT.detect", + "klass": "DEFECT", + "value": "changeset pr: value mismatches the actual PR number returned by gh api POST /pulls", + "line": 670 + }, + { + "id": "DEFECT.CHANGESET-PR-FIELD-DRIFT.examples", + "klass": "DEFECT", + "value": "#3316 (pr:3312 was the issue), #3325 (pr:3319 was a guess); already covered in CONTEXT.md L94 + L186 but recurs every cycle", + "line": 669 + }, + { + "id": "DEFECT.CHANGESET-PR-FIELD-DRIFT.fix-forward", + "klass": "DEFECT", + "value": "author changeset with placeholder pr:0; immediately after gh api POST /pulls returns the number, edit changeset and amend or follow-up commit; never guess", + "line": 671 + }, + { + "id": "DEFECT.CHANGESET-PR-FIELD-DRIFT.symptom", + "klass": "DEFECT", + "value": ".changeset/*.md frontmatter pr: value is the issue number, a guess made before PR opened, or a stale stacked-PR number", + "line": 668 + }, + { + "id": "DEFECT.DEFAULT-FLIP-DOCUMENTATION.detect", + "klass": "DEFECT", + "value": "any PR that changes a default value in CONFIG_DEFAULTS or buildNewProjectConfig; check that PR body Breaking Changes section explicitly covers (a) when the new default takes effect, (b) opt-back-in command, (c) effect on in-flight artifacts", + "line": 705 + }, + { + "id": "DEFECT.DEFAULT-FLIP-DOCUMENTATION.examples", + "klass": "DEFECT", + "value": "#3309 v2 default flip from mid-flight to end-of-phase", + "line": 704 + }, + { + "id": "DEFECT.DEFAULT-FLIP-DOCUMENTATION.fix-forward", + "klass": "DEFECT", + "value": "template — \"new default takes effect when .planning/config.json is rewritten (config-set, fresh project, regenerated config); existing artifacts continue to work; opt-back-in: gsd config-set \"", + "line": 706 + }, + { + "id": "DEFECT.DEFAULT-FLIP-DOCUMENTATION.symptom", + "klass": "DEFECT", + "value": "PR flips a config default but does not call out the migration semantics (when does the new default take effect; existing configs vs new configs; what the opt-back-in looks like)", + "line": 703 + }, + { + "id": "DEFECT.FORMAT", + "klass": "DEFECT", + "value": "class.sub-key=value | classes are greppable; each class carries detect / fix / anchor sub-keys when applicable", + "line": 620 + }, + { + "id": "DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.detect", + "klass": "DEFECT", + "value": "grep \"^:\" on a *.md whose result is compared to exact tokens, with no frontmatter scoping and no -m1; one body line beginning : is enough to break it", + "line": 718 + }, + { + "id": "DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.examples", + "klass": "DEFECT", + "value": "#586/PR #650 ship.md verification gate — grep \"^status:\" also matched body status: lines, yielding passed+gaps_found+human_needed instead of passed and blocking a passed phase; the same broad-grep still lives in execute-phase.md (consolidation tracked by #651)", + "line": 717 + }, + { + "id": "DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.fix-forward", + "klass": "DEFECT", + "value": "scope to the leading frontmatter block and take the first match: sed -n '/^---$/,/^---$/p' \"$f\" | grep -m1 \"^:\" | cut -d: -f2 | tr -d ' '; fix every parallel copy in the same change or consolidate behind one queryable seam (#651)", + "line": 719 + }, + { + "id": "DEFECT.FRONTMATTER-SCALAR-BROAD-GREP.symptom", + "klass": "DEFECT", + "value": "a YAML-frontmatter scalar (e.g. VERIFICATION.md status) read with grep \"^key:\" over the WHOLE markdown report instead of the frontmatter block; a key: line in the body (code block, copied artifact, example) returns extra matches that concatenate after cut|tr into a value matching no expected token, so a valid state is misrouted", + "line": 716 + }, + { + "id": "DEFECT.GENERATIVE-EXEMPLAR", + "klass": "DEFECT", + "value": "tests/runtime-launcher-parity.test.cjs (asserts every workflow bash block uses the canonical gsd_run launcher — the in-repo pattern for enforcing equality across parallel surfaces)", + "line": 714 + }, + { + "id": "DEFECT.GENERATIVE-FIX", + "klass": "DEFECT", + "value": "for any new constant/array/parser shared between two parallel surfaces (two workflow surfaces, or a generated artifact and its hand-authored source), the same commit MUST add a parity assertion that fails when the two diverge", + "line": 713 + }, + { + "id": "DEFECT.GENERATIVE-PRIORITY", + "klass": "DEFECT", + "value": "these defect classes share a common root: parallel implementations diverge silently because no parity test enforces equality at the test layer", + "line": 712 + }, + { + "id": "DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.detect", + "klass": "DEFECT", + "value": "two gsd-test-summary --both runs in flight; UnicodeDecodeError in parse_events_from_string traceback; /tmp/gsd-test-*.jsonl size mismatch vs total events emitted", + "line": 855 + }, + { + "id": "DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.fix-forward", + "klass": "DEFECT", + "value": "set per-invocation LOCAL_OUT=/tmp/gsd-test--local.jsonl DOCKER_OUT=/tmp/gsd-test--docker.jsonl env vars; or serialize the runs; upstream fix tracked in #3545 (default to tempfile.mkstemp + advisory flock)", + "line": 856 + }, + { + "id": "DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.root-cause", + "klass": "DEFECT", + "value": "gsd-test-summary lines 126-127 default LOCAL_OUT/DOCKER_OUT to fixed /tmp/gsd-test-{local,docker}.jsonl; concurrent line-buffered writers interleave bytes mid-multibyte → split UTF-8 sequence → decoder explodes on f.read()", + "line": 854 + }, + { + "id": "DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.symptom", + "klass": "DEFECT", + "value": "two simultaneous gsd-test-summary --both invocations (e.g. one per worktree) both crash with UnicodeDecodeError in parse_events_from_file; \"local exit=1 docker exit=1\" reported even though remote containers ran fine", + "line": 853 + }, + { + "id": "DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION.upstream", + "klass": "DEFECT", + "value": "open-gsd/gsd-core#3545", + "line": 857 + }, + { + "id": "DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.detect", + "klass": "DEFECT", + "value": "gsd-test-summary's task output file at /private/tmp/claude-*/tasks/.output stays 0 bytes for >5 min after launch; ps shows the test still alive; ssh -o ConnectTimeout=5 true now times out", + "line": 823 + }, + { + "id": "DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.examples", + "klass": "DEFECT", + "value": "2026-05-16 redshirt probed up at 12:48 UTC, gsd-test-summary picked it, docker container spawned, then redshirt's ssh daemon stopped responding — banner-exchange timeout. Test stalled 20+ minutes with the wrapper's output file at 0 bytes", + "line": 822 + }, + { + "id": "DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.fix-forward", + "klass": "DEFECT", + "value": "TaskStop the wrapper; pkill -f gsd-test-summary + pkill -f \"ssh \"; re-run gsd-test-summary so pick_host re-randomizes from the live set (probe each ~/.config/gsd-test/hosts entry first to confirm). Upstream fix candidate: gsd-test should add a heartbeat read on the ssh-stdin channel and abort + retry on a different host after N silent seconds", + "line": 824 + }, + { + "id": "DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.related", + "klass": "DEFECT", + "value": "DEFECT.GSD-TEST-MIRROR-POISONED (legacy bind-mount ownership); GSD-TEST-CONCURRENT-OUTPUT-COLLISION (file collision) — host-mid-run-death is the third independent gsd-test infra failure mode this month", + "line": 825 + }, + { + "id": "DEFECT.GSD-TEST-HOST-MID-RUN-DEATH.symptom", + "klass": "DEFECT", + "value": "pick_host succeeds at probe time (ssh -o ConnectTimeout=3 -o BatchMode=yes \"$h\" true); subsequent ssh \"$h\" 'docker run ...' hangs indefinitely because the chosen host went unreachable between probe and exec; gsd-test-summary buffers stderr until the wrapper exits, so the operator sees no progress at all", + "line": 821 + }, + { + "id": "DEFECT.GSD-TEST-MIRROR-POISONED.detect", + "klass": "DEFECT", + "value": "docker stderr shows rsync: [generator] delete_file: unlink(...) failed: Permission denied (13) OR [receiver] mkstemp \".gsd-*.\" failed", + "line": 847 + }, + { + "id": "DEFECT.GSD-TEST-MIRROR-POISONED.recovery", + "klass": "DEFECT", + "value": "ssh 'docker run --rm -v ~/gsd-mirror-gsd-core:/work gsd-test:node22 chown -R : /work'; remote-uid is the SSH user's uid on the remote (1000 on holodeck, NOT local Mac 501)", + "line": 849 + }, + { + "id": "DEFECT.GSD-TEST-MIRROR-POISONED.root-cause", + "klass": "DEFECT", + "value": "container ran without --user; build:hooks wrote into bind-mount as root; chown-back-before-exec patch closes forward path but not legacy hosts", + "line": 848 + }, + { + "id": "DEFECT.GSD-TEST-MIRROR-POISONED.symptom", + "klass": "DEFECT", + "value": "gsd-test-summary --both exits docker=23 (rsync partial transfer) with mkstemp Permission denied on remote mirror files; mirror has root-owned artifacts from prior cold runs", + "line": 846 + }, + { + "id": "DEFECT.GSD-TEST-MIRROR-POISONED.upstream", + "klass": "DEFECT", + "value": "trek-e/gsd-test-runner#1 — proposes self-healing init-time chown probe", + "line": 850 + }, + { + "id": "DEFECT.HALT-COST-PATTERN.detect", + "klass": "DEFECT", + "value": "any subagent-spawning workflow with mid-flight pause-and-resume that does not preserve subagent context", + "line": 695 + }, + { + "id": "DEFECT.HALT-COST-PATTERN.examples", + "klass": "DEFECT", + "value": "#3309 checkpoint:human-verify (mid-flight halt = full executor cold-start per round-trip; reporter measured \"tens of thousands of tokens\" per halt)", + "line": 694 + }, + { + "id": "DEFECT.HALT-COST-PATTERN.fix-forward", + "klass": "DEFECT", + "value": "offer config flag for end-of-phase aggregation; if cost dominates make end-of-phase the default; route deferred items through existing verifier surface, do not invent new writer", + "line": 696 + }, + { + "id": "DEFECT.HALT-COST-PATTERN.symptom", + "klass": "DEFECT", + "value": "architecturally-sound checkpoint pattern produces hidden token cost because subagent context is discarded across the pause and respawn", + "line": 693 + }, + { + "id": "DEFECT.HOOK-OVER-ENFORCEMENT.detect", + "klass": "DEFECT", + "value": "hook re-fires on each invocation regardless of session-state read receipts", + "line": 700 + }, + { + "id": "DEFECT.HOOK-OVER-ENFORCEMENT.examples", + "klass": "DEFECT", + "value": "this session repeatedly hit \"Refusing to run gh issue create|edit / gh pr create|edit\" despite reading every listed file", + "line": 699 + }, + { + "id": "DEFECT.HOOK-OVER-ENFORCEMENT.fix-forward", + "klass": "DEFECT", + "value": "use gh api -X PATCH repos/{owner}/{repo}/pulls/{N} or repos/{owner}/{repo}/issues/{N} directly — same effect, hook regex does not match", + "line": 701 + }, + { + "id": "DEFECT.HOOK-OVER-ENFORCEMENT.read-tool-tracking", + "klass": "DEFECT", + "value": "gh-templates-first PreToolUse hook tracks Read tool invocations specifically; Bash cat/head of the same file does NOT satisfy the hook; future-self must use Read tool from the first contact with template files", + "line": 852 + }, + { + "id": "DEFECT.HOOK-OVER-ENFORCEMENT.symptom", + "klass": "DEFECT", + "value": "PreToolUse hook keeps blocking gh pr edit / gh issue edit even after all required files are read in the session", + "line": 698 + }, + { + "id": "DEFECT.HOOK-OVER-ENFORCEMENT.write-bypass", + "klass": "DEFECT", + "value": "security_reminder_hook can block Write on substring match (e.g. a literal child-process call-expression token); workaround is heredoc to /tmp then mv into place, or use Edit instead — Edit hooks are more lenient than Write hooks", + "line": 871 + }, + { + "id": "DEFECT.INVENTORY-DRIFT.detect", + "klass": "DEFECT", + "value": "tests/inventory-manifest-sync.test.cjs fails with \"New surfaces not in manifest\"; tests/inventory-headings-countfree.test.cjs fails if a (N shipped) count is re-added to a heading", + "line": 660 + }, + { + "id": "DEFECT.INVENTORY-DRIFT.examples", + "klass": "DEFECT", + "value": "#3309 planner-human-verify-mode.md (caught by tests/inventory-manifest-sync.test.cjs)", + "line": 659 + }, + { + "id": "DEFECT.INVENTORY-DRIFT.fix-forward", + "klass": "DEFECT", + "value": "update INVENTORY.md row entry; run node scripts/gen-inventory-manifest.cjs --write to regen INVENTORY-MANIFEST.json (all six families.* arrays are canonical — see RULESET.MANIFEST-CANONICAL-KEY)", + "line": 661 + }, + { + "id": "DEFECT.INVENTORY-DRIFT.symptom", + "klass": "DEFECT", + "value": "new file added under gsd-core/references/ or gsd-core/workflows/ without updating docs/INVENTORY.md row AND docs/INVENTORY-MANIFEST.json", + "line": 658 + }, + { + "id": "DEFECT.NAME-COLLISION.detect", + "klass": "DEFECT", + "value": "trace every CLI/test caller of the canonical name → if any caller's argv shape differs from the rebound handler's args[0] expectation, the migration broke the legacy contract", + "line": 795 + }, + { + "id": "DEFECT.NAME-COLLISION.examples", + "klass": "DEFECT", + "value": "#3577 config-ensure-section (legacy = no-arg full-default init via ensureConfigFile→buildNewProjectConfig; the rebound configEnsureSection = single-section ensure requiring args[0]; all CLI callers pass no args; handler throws \"Usage: config-ensure-section
\")", + "line": 794 + }, + { + "id": "DEFECT.NAME-COLLISION.fix-forward", + "klass": "DEFECT", + "value": "either (a) bind the dispatch to a handler whose body mirrors legacy semantics (e.g. configNewProject when no args), or (b) keep the dispatch case calling the original handler directly (precedent: 7d5dfa9d codex runtime carve-out). Whichever path, add a behavioral test that round-trips the legacy invocation shape to lock the contract", + "line": 796 + }, + { + "id": "DEFECT.NAME-COLLISION.symptom", + "klass": "DEFECT", + "value": "a router migration rebinds CLI dispatch for a canonical command name to a handler with a different positional-arg shape; every legacy no-arg / wrong-arg caller then errors out at the new handler's own validation throw", + "line": 793 + }, + { + "id": "DEFECT.PARSER-BRITTLE-MARKER-WHITELIST.detect", + "klass": "DEFECT", + "value": "any parser with hard-coded marker list; any parser that returns empty for non-matching input without warning", + "line": 690 + }, + { + "id": "DEFECT.PARSER-BRITTLE-MARKER-WHITELIST.examples", + "klass": "DEFECT", + "value": "ac518646/#3263 code-review SUMMARY parser rejected BL-/blocker variants", + "line": 689 + }, + { + "id": "DEFECT.PARSER-BRITTLE-MARKER-WHITELIST.fix-forward", + "klass": "DEFECT", + "value": "accept variants explicitly (case-insensitive, hyphen/space alternatives); on unknown marker emit a structured WARN with the original line so the human can fix the source", + "line": 691 + }, + { + "id": "DEFECT.PARSER-BRITTLE-MARKER-WHITELIST.symptom", + "klass": "DEFECT", + "value": "human-output parser whitelists known markers (severity, status); silently drops unfamiliar markers as malformed", + "line": 688 + }, + { + "id": "DEFECT.PHASE-DIR-PREFIX-DRIFT.anchor", + "klass": "DEFECT", + "value": "tests/bug-3298-phase-dir-prefix-drift-in-workflows.test.cjs (broad regression across workflow surfaces)", + "line": 636 + }, + { + "id": "DEFECT.PHASE-DIR-PREFIX-DRIFT.detect", + "klass": "DEFECT", + "value": "grep mkdir/touch/path.join with {NN}-{slug} or padded_phase + phase_slug; if not consuming expected_phase_dir from init.* JSON it is drifting", + "line": 634 + }, + { + "id": "DEFECT.PHASE-DIR-PREFIX-DRIFT.examples", + "klass": "DEFECT", + "value": "#3287 (init.phase-op + init.plan-phase first-touch), #3306/PRED.k015 (plan-milestone-gaps + import + add-backlog), #3297/#3298 (sibling reports)", + "line": 633 + }, + { + "id": "DEFECT.PHASE-DIR-PREFIX-DRIFT.fix-forward", + "klass": "DEFECT", + "value": "consume expected_phase_dir from init.phase-op / init.plan-phase output; never re-construct from padded_phase + slug in workflow steps", + "line": 635 + }, + { + "id": "DEFECT.PHASE-DIR-PREFIX-DRIFT.symptom", + "klass": "DEFECT", + "value": "multiple workflow files independently construct .planning/phases/{NN}-{slug} paths; project_code prefix or slug normalization missing in some surfaces", + "line": 632 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.detect", + "klass": "DEFECT", + "value": "CI security lane (Prompt injection scan step) reports FAIL: tests/.test.cjs with a line number pointing at a string literal; the literal is inside an assert.throws() or array of malicious inputs; the test file name is not in scripts/prompt-injection-scan.sh ALLOWLIST", + "line": 747 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.examples", + "klass": "DEFECT", + "value": "PR #1622 commit 4ed208e74 added convertClaudeCommandToWindsurfWorkflow commandName validation with 22 malicious-name fixtures; scanner matched an instruction-override phrase at tests/windsurf-conversion.test.cjs:122; CI security lane failed even though the test is the security control", + "line": 746 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.fix-forward", + "klass": "DEFECT", + "value": "ADD the test file to scripts/prompt-injection-scan.sh ALLOWLIST array with a comment citing this defect class; for large fixture sets, move them to tests/fixtures/adversarial/security/ (auto-allowlisted dir) and load via readFileSync; never weaken or fragment the payload to evade the scanner — that defeats the test's purpose; ALSO when documenting this defect in CONTEXT.md, do NOT quote the literal pattern — describe it generically (the scanner scans CONTEXT.md too)", + "line": 748 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.prevention", + "klass": "DEFECT", + "value": "when writing a security regression test that uses real injection payloads as fixtures, immediately add the test file path to scripts/prompt-injection-scan.sh ALLOWLIST in the same commit; when documenting this defect class anywhere under scanner scope (CONTEXT.md, docs/, agent .md), use descriptive references like 'scanner-matching payload' rather than quoting the literal pattern; ref DEFECT.PROMPT-INJECTION-SCAN-COLLISION (the older XML-tag-collision variant)", + "line": 749 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.symptom", + "klass": "DEFECT", + "value": "scripts/prompt-injection-scan.sh flags a NEW test file as a finding because the test contains real injection payloads as fixtures (strings that match one of the scanner's PATTERNS — see scripts/prompt-injection-scan.sh lines 18-64) to prove the validator under test rejects them; scanner cannot distinguish fixture from real injection; CI security lane fails on the test that ADDS the security validation", + "line": 745 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION.detect", + "klass": "DEFECT", + "value": "any new bare tag in agents/*.md", + "line": 655 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION.examples", + "klass": "DEFECT", + "value": "#3309 added a bare 'human' element (angle-bracket-wrapped) for verify-block harvesting; tests/prompt-injection-scan.security.test.cjs flags angle-bracket-wrapped names matching system|assistant|human (open or close form)", + "line": 654 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION.fix-forward", + "klass": "DEFECT", + "value": "hyphenate the tag (, ) — scanner regex matches bare names only", + "line": 656 + }, + { + "id": "DEFECT.PROMPT-INJECTION-SCAN-COLLISION.symptom", + "klass": "DEFECT", + "value": "custom XML element name in agent .md file matches scripts/scan-prompt-injection regex; legitimate agent vocabulary trips the security gate", + "line": 653 + }, + { + "id": "DEFECT.REMOVED-BUT-NEEDED.detect", + "klass": "DEFECT", + "value": "before deletion, grep filename across .github/workflows, gsd-core/, docs/, package.json scripts; if any reference exists removal is incomplete", + "line": 624 + }, + { + "id": "DEFECT.REMOVED-BUT-NEEDED.examples", + "klass": "DEFECT", + "value": "#3316 root package-lock.json (root package.json declares deps; workflows use cache:'npm' + npm ci), e3b52c70 docs referenced removed /gsd-new-workspace", + "line": 623 + }, + { + "id": "DEFECT.REMOVED-BUT-NEEDED.fix-forward", + "klass": "DEFECT", + "value": "restore the file or update every consumer in the same commit; do not paper over with --no-package-lock or workflow workarounds that lose reproducibility", + "line": 625 + }, + { + "id": "DEFECT.REMOVED-BUT-NEEDED.symptom", + "klass": "DEFECT", + "value": "file/key removed because \"no longer used\" without verifying every consumer (workflows, docs, manifests, npm scripts)", + "line": 622 + }, + { + "id": "DEFECT.RESEARCH-PROVIDER-PROSE-DRIFT", + "klass": "DEFECT", + "value": "provider waterfall duplicated across N researcher agent .md files drifts independently (META.RULE.brief-no-paraphrase); fix-forward=research-provider.cjs single source of truth + generated agents (#657)", + "line": 285 + }, + { + "id": "DEFECT.SCOPE.window", + "klass": "DEFECT", + "value": "PRs #3306..#3325 + sibling fixes #3240/#3242/#3245/#3257/#3261/#3267/#3286/#3287", + "line": 619 + }, + { + "id": "DEFECT.SDK-PORT-NAME-COLLISION.generative-tie", + "klass": "DEFECT", + "value": "instance of DEFECT.GENERATIVE-PRIORITY — parity assertion at the test layer between CJS handler shape and SDK handler shape would have failed at PR open", + "line": 797 + }, + { + "id": "DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.detect", + "klass": "DEFECT", + "value": "grep tests for fs.unlinkSync|rmSync|writeFileSync|renameSync|cpSync targeting paths resolved from the repo root (join(__dirname,'..',...)) under gsd-core/bin/lib or a shared committed fixture, instead of a mkdtempSync temp dir; any build helper (e.g. ensureBuiltArtifacts) invoked with real-tree paths during the concurrent test phase; any tsBuildInfoFile / build-cache path that lands inside a copied/shipped dir (gsd-core/bin/)", + "line": 807 + }, + { + "id": "DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.examples", + "klass": "DEFECT", + "value": "#996/88e30d53 — bug-969 hardening tests fs.unlinkSync'd + restored the real gsd-core/bin/lib/core.cjs and set tsBuildInfoFile inside gsd-core/bin/ → next red across the full-test matrix (macOS/Windows) + ubuntu-24 coverage leg, ~40-50 MODULE_NOT_FOUND/ENOENT per leg; reproduced locally on iteration 1; fixed #1001/#1002", + "line": 806 + }, + { + "id": "DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.fix-forward", + "klass": "DEFECT", + "value": "tests mutate ONLY isolated mkdtempSync copies — never delete/rewrite shared real build outputs while node --test runs files concurrently; parameterize build helpers to accept {root,srcDir,outDir,tsBuildInfoPath,tsconfigPath} overrides and point the test at a throwaway temp project (precedent: #1002 ensureBuiltArtifacts(overrides)); keep mutable build state (tsbuildinfo) OUTSIDE copied/shipped trees (repo root, gitignored) + best-effort self-heal of stale bin-local copies; this is the concrete instance of the RULESET.TESTS.delete-bad-tests real-race class", + "line": 808 + }, + { + "id": "DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.symptom", + "klass": "DEFECT", + "value": "a test deletes/rewrites a SHARED REAL build artifact or fixture (e.g. gsd-core/bin/lib/*.cjs, the build tsbuildinfo) that other test files require; node --test runs files concurrently, so innocent concurrent tests intermittently fail with \"Cannot find module\" / ENOENT while the racy test itself passes (victim-not-culprit, leg-asymmetric red); placing mutable build state inside a copied/shipped tree (gsd-core/bin/) additionally races install-test fs.cpSync copies → copyfile ENOENT", + "line": 805 + }, + { + "id": "DEFECT.SHARED-ARTIFACT-MUTATION-IN-CONCURRENT-TEST.test-anchor", + "klass": "DEFECT", + "value": "tests/bug-969-test-infra-flake-hardening.test.cjs (hermetic temp-project rewrite); regression gate = 10x concurrent run of that suite + tests/state.test.cjs + tests/install.test.cjs must be clean (reproduces on iter 1 when racy)", + "line": 809 + }, + { + "id": "DEFECT.SOURCE-GREP-IN-NEW-TESTS.detect", + "klass": "DEFECT", + "value": "scripts/lint-no-source-grep.cjs (npm run lint:tests) fails with line-number-precise violation", + "line": 709 + }, + { + "id": "DEFECT.SOURCE-GREP-IN-NEW-TESTS.fix-forward", + "klass": "DEFECT", + "value": "replace with runGsdTools(...) behavioral test capturing JSON; if asserting agent .md content (which IS the runtime contract) add // allow-test-rule: source-text-is-the-product with one-line justification", + "line": 710 + }, + { + "id": "DEFECT.SOURCE-GREP-IN-NEW-TESTS.symptom", + "klass": "DEFECT", + "value": "new test file uses readFileSync + .includes() / .match() against source code (CONTEXT.md L82); contradicts the test rule lint script", + "line": 708 + }, + { + "id": "DEFECT.STACKED-PR-AUTO-RETARGET.detect", + "klass": "DEFECT", + "value": "ls-remote shows base ref absent; PR base still points at the deleted ref; mergeable=CONFLICTING with no real diff conflicts", + "line": 640 + }, + { + "id": "DEFECT.STACKED-PR-AUTO-RETARGET.examples", + "klass": "DEFECT", + "value": "#3311 base fix/3255-add-json-errors-mode-gsd-tools deleted after #3304 merged", + "line": 639 + }, + { + "id": "DEFECT.STACKED-PR-AUTO-RETARGET.fix-forward", + "klass": "DEFECT", + "value": "PATCH /repos/{owner}/{repo}/pulls/{N} -f base=main; rebase head onto current main; resolve carry-over commits (parent commits will auto-drop as patch contents already upstream)", + "line": 641 + }, + { + "id": "DEFECT.STACKED-PR-AUTO-RETARGET.symptom", + "klass": "DEFECT", + "value": "PR #N is stacked on branch B; branch B merges to main and is deleted; GitHub does not reliably auto-retarget #N to main; PR shows DIRTY/CONFLICTING with phantom conflicts", + "line": 638 + }, + { + "id": "DEFECT.STACKED-PR-CANNOT-STAND-ALONE.anti-pattern", + "klass": "DEFECT", + "value": "blindly running git rebase --onto origin/main on the patch branch — produces \"conflicts\" that are really \"the scaffolding doesn't exist yet\"; resolving them means reinventing the upstream PR's contribution, which duplicates work and creates merge hazards. Recognize the shape early via cat-file probe before rebasing", + "line": 815 + }, + { + "id": "DEFECT.STACKED-PR-CANNOT-STAND-ALONE.detect", + "klass": "DEFECT", + "value": "gh pr view --json baseRefName shows non-main base; OR git rebase --onto origin/main produces real (not whitespace) conflicts at files the patch claims to modify; OR git cat-file -e origin/main: errors with \"does not exist in origin/main\"", + "line": 813 + }, + { + "id": "DEFECT.STACKED-PR-CANNOT-STAND-ALONE.examples", + "klass": "DEFECT", + "value": "#3639 + #3637 both targeted base=feat/3575-enforcement-hardening (the Phase 6 PR #3577); #3639 modifies SDK-bridge calls in 6 family-router files that on main do NOT have any SDK-bridge call yet; #3637 patches scripts/lint-shared-module-handsync.cjs which does not exist on main at all", + "line": 812 + }, + { + "id": "DEFECT.STACKED-PR-CANNOT-STAND-ALONE.fix-forward", + "klass": "DEFECT", + "value": "user policy (this session, 2026-05-16): every PR must stand alone. Resolution = cherry-pick the patch's unique commits onto the upstream PR head, push to upstream PR branch, close patch PR with \"subsumed by #\". Alternatives explicitly rejected: leaving stacked open (\"no, fold them in\") and closing-without-folding (\"we want the fix\")", + "line": 814 + }, + { + "id": "DEFECT.STACKED-PR-CANNOT-STAND-ALONE.symptom", + "klass": "DEFECT", + "value": "patch PR was authored against scaffolding (handler files, lint scripts, generated modules) that exists only on an unmerged upstream feature branch; the PR's \"base\" on GitHub is the feature branch, not main; merging requires the upstream PR to land first", + "line": 811 + }, + { + "id": "DEFECT.STATE-TRAMPLE.detect", + "klass": "DEFECT", + "value": "any state writer that calls buildStateFrontmatter without preserving existing progress.* keys; any mutation surface that does not honor shouldPreserveExistingProgress", + "line": 629 + }, + { + "id": "DEFECT.STATE-TRAMPLE.examples", + "klass": "DEFECT", + "value": "#3242 (Last Activity overwrote progress.completed_plans), #3257 (nested plans/ files uncounted), #3261 (buildStateFrontmatter), #3265 (canonical fields), #3286 (record-metric/add-decision sections)", + "line": 628 + }, + { + "id": "DEFECT.STATE-TRAMPLE.fix-forward", + "klass": "DEFECT", + "value": "route through state-document.cjs/.ts shouldPreserveExistingProgress + normalizeProgressNumbers (extracted in #3316 SDK-first seams)", + "line": 630 + }, + { + "id": "DEFECT.STATE-TRAMPLE.symptom", + "klass": "DEFECT", + "value": "state-mutation paths overwrite curated values when body-derived computation is narrower than what's stored in frontmatter", + "line": 627 + }, + { + "id": "DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.anchor", + "klass": "DEFECT", + "value": "project CLAUDE.md \"Top-level orchestrator (cross-turn notifications available) vs Sub-agent worker (no cross-turn notifications)\" guidance — load-bearing for multi-worktree parallel fix dispatch", + "line": 861 + }, + { + "id": "DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.detect", + "klass": "DEFECT", + "value": "sub-agent returns prematurely with text like \"I should wait for the notification per CLAUDE.md\" and incomplete work in its worktree (commits absent, push absent, PR absent)", + "line": 859 + }, + { + "id": "DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.fix-forward", + "klass": "DEFECT", + "value": "keep gsd-test-summary --both at the top-level orchestrator; sub-agents either run it foreground with timeout: 1500000 (25min) and block, OR delegate the test step back to the orchestrator (write commits + return); never have a sub-agent fire-and-await a backgrounded long task", + "line": 860 + }, + { + "id": "DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL.symptom", + "klass": "DEFECT", + "value": "spawned sub-agent kicks off gsd-test-summary --both via Bash run_in_background, then stops on the harness \"you will be notified\" message; never receives the notification because cross-turn task-notifications are only delivered to the top-level orchestrator", + "line": 858 + }, + { + "id": "DEFECT.SUPERSEDED-CONCURRENT-PRS.detect", + "klass": "DEFECT", + "value": "after a fix lands on main, grep recently-merged PR title for shared keyword/issue; check open PRs touching same files; if open PRs are subsets of merged work they are superseded", + "line": 650 + }, + { + "id": "DEFECT.SUPERSEDED-CONCURRENT-PRS.examples", + "klass": "DEFECT", + "value": "#3303 + #3307 superseded by #3306 (all addressing #3297/#3298 project_code prefix family)", + "line": 649 + }, + { + "id": "DEFECT.SUPERSEDED-CONCURRENT-PRS.fix-forward", + "klass": "DEFECT", + "value": "close superseded PRs via gh api PATCH state=closed; do not comment on self-authored PRs (k101); the link to the merged PR makes supersession discoverable in PR history", + "line": 651 + }, + { + "id": "DEFECT.SUPERSEDED-CONCURRENT-PRS.symptom", + "klass": "DEFECT", + "value": "multiple in-flight PRs attack overlapping subsets of the same issue; the broadest one merges first; narrower siblings remain open with phantom conflicts", + "line": 648 + }, + { + "id": "DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.detect", + "klass": "DEFECT", + "value": "test does readFileSync(md).match for a bash fence with literal \\n, OR execFileSync('bash',...) gated only on a bash-presence probe; also verifying a new test with a file-scoped run instead of the full suite hides repo-wide static guards", + "line": 722 + }, + { + "id": "DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.examples", + "klass": "DEFECT", + "value": "#586/PR #650 tests/ship-586-verification-routing.test.cjs — the fence \\n offender failed ubuntu-24/macos/coverage, then the Windows tmpdir-path glob failed full test (windows-latest,22) at fail 3; both were invisible to file-scoped gsd-test-both runs because the parity guard is only scanned by the full suite", + "line": 721 + }, + { + "id": "DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.fix-forward", + "klass": "DEFECT", + "value": "match the fence with \\r?\\n and normalize the captured block to LF; gate pipeline execution on process.platform !== 'win32' && hasBash since the extraction LOGIC is platform-independent and POSIX coverage suffices; run the full suite (or the parity/lint guards) before push when adding a test file", + "line": 723 + }, + { + "id": "DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.symptom", + "klass": "DEFECT", + "value": "a test that parses a workflow bash block out of a *.md and runs it via execFileSync('bash',...) breaks on Windows two ways: the fence regex uses a literal \\n after the bash fence that will not match CRLF and trips windows-test-parity-guard (fenceRegexLiteralNewline); and git-bash exists so a bash-presence probe is true, but an os.tmpdir() Windows path (C:\\...) is un-globbable in bash so the pipeline returns empty and assertions fail", + "line": 720 + }, + { + "id": "DEFECT.UNBOUNDED-SUBPROCESS.detect", + "klass": "DEFECT", + "value": "execSync/execFileSync/spawnSync without timeout option in non-test code; especially git list-worktrees, git fetch, npm view", + "line": 685 + }, + { + "id": "DEFECT.UNBOUNDED-SUBPROCESS.examples", + "klass": "DEFECT", + "value": "a33cbe72 worktree fix bound git subprocesses with timeout", + "line": 684 + }, + { + "id": "DEFECT.UNBOUNDED-SUBPROCESS.fix-forward", + "klass": "DEFECT", + "value": "add timeout (5-30s for git, 60s for npm); on timeout return degraded result + structured warning rather than throw", + "line": 686 + }, + { + "id": "DEFECT.UNBOUNDED-SUBPROCESS.symptom", + "klass": "DEFECT", + "value": "git/npm subprocess shelled out without timeout; CLI hangs indefinitely on stuck remote, large repo, or missing network", + "line": 683 + }, + { + "id": "DEFECT.WINDOWS-ARGV-OVERFLOW.detect", + "klass": "DEFECT", + "value": "Windows CI job at \"Run unit tests\" exits with code 1 within seconds of starting, no node:test output between \"run-tests: suite=… files=N: …\" line and \"Process completed with exit code 1\"; same job on Linux/macOS runs full duration", + "line": 801 + }, + { + "id": "DEFECT.WINDOWS-ARGV-OVERFLOW.examples", + "klass": "DEFECT", + "value": "#3649 scripts/run-tests.cjs spawning 546 paths (~85 chars each ≈ 46 KB); Linux ARG_MAX 2 MB allows it, Windows aborts in ~70 ms with zero test output making the failure look like the runner itself crashed", + "line": 800 + }, + { + "id": "DEFECT.WINDOWS-ARGV-OVERFLOW.fix-forward", + "klass": "DEFECT", + "value": "chunk argv into batches whose total length stays under 28,000 chars (headroom under the 32,767 ceiling); run each chunk sequentially; aggregate exit codes (first non-zero wins). Expose RUN_TESTS_MAX_CMDLINE_CHARS env override so cross-platform regression tests can force chunking with short tmp paths", + "line": 802 + }, + { + "id": "DEFECT.WINDOWS-ARGV-OVERFLOW.symptom", + "klass": "DEFECT", + "value": "execFileSync(node, ['--test', ...N paths]) succeeds on Linux/macOS, instantly exits with code 1 and no test output on Windows when N×avg(path_len) exceeds 32,767 chars (CreateProcess lpCommandLine cap)", + "line": 799 + }, + { + "id": "DEFECT.WINDOWS-ARGV-OVERFLOW.test-anchor", + "klass": "DEFECT", + "value": "tests/run-tests-harness.test.cjs \"Windows argv-overflow chunking (issue #3597)\" — 30 long-named fixture files + RUN_TESTS_MAX_CMDLINE_CHARS=2000 → asserts run-tests: chunk N/M marker in stderr; pattern works on every platform", + "line": 803 + }, + { + "id": "DEFECT.WINDOWS-FS-OPS.detect", + "klass": "DEFECT", + "value": "any rename/copy in build/install path without try/catch fallback", + "line": 680 + }, + { + "id": "DEFECT.WINDOWS-FS-OPS.examples", + "klass": "DEFECT", + "value": "c47c2c5d build-hooks rename → copy fallback, d2412271 install Windows persistent SDK shim", + "line": 679 + }, + { + "id": "DEFECT.WINDOWS-FS-OPS.fix-forward", + "klass": "DEFECT", + "value": "catch EPERM/EBUSY/EACCES, fall back to copy + unlink with retry, surface degraded-mode message; never silently swallow", + "line": 681 + }, + { + "id": "DEFECT.WINDOWS-FS-OPS.symptom", + "klass": "DEFECT", + "value": "fs.renameSync / fs.copyFileSync hits EPERM/EBUSY on Windows when antivirus or another process holds a transient handle on the target", + "line": 678 + }, + { + "id": "DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.detect", + "klass": "DEFECT", + "value": "any function returning a filesystem path that flows into markdown/text body substitution; grep for path.join/raw resolvedTarget/${configDir}/ in code paths writing workflow .md, agent .md, or generated docs; smoke pattern is ${resolvedTarget}/ or ${configDir}/... templates that bypass normalization", + "line": 739 + }, + { + "id": "DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.examples", + "klass": "DEFECT", + "value": "PR #1622 computePathPrefix returned ${resolvedTarget}/ verbatim — rewrites of @~/.claude/gsd-core/commands/gsd/X.md wrote @C:\\...\\gsd-ial-windsurf-XXX\\gsd-core/commands/gsd/help.md (trailing forward slashes from the original literal survived, prefix backslashes did not); tests/install-runtime-artifacts.test.cjs:318 + tests/install.test.cjs:1323 failed on windows-latest only", + "line": 738 + }, + { + "id": "DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.fix-forward", + "klass": "DEFECT", + "value": "normalize at the SOURCE not the test: posixTarget=String(resolvedTarget).replace(/\\\\/g,'/'), posixHome=homeDir?String(homeDir).replace(/\\\\/g,'/'):homeDir; markdown body is POSIX-only; .replace(/\\\\/g,'/') is idempotent on POSIX (no backslashes present) so safe to apply unconditionally; isWindowsHost arg is a no-op tripwire (enh-1511) — do NOT branch on it, normalize always", + "line": 740 + }, + { + "id": "DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.prevention", + "klass": "DEFECT", + "value": "RULESET.CONTENT-PATH-NORMALIZATION; tests are downstream signal, never the fix; ref DEFECT.WINDOWS-TEST-PORTABILITY for test-side parity (normalize expected substrings too: ${configDir}/foo.replace(/\\\\/g,'/'))", + "line": 741 + }, + { + "id": "DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.symptom", + "klass": "DEFECT", + "value": "path.join() result on Windows (backslashes) substituted verbatim into markdown body (@-references, workflow files, generated docs); content gains mixed separators; cross-platform substring assertions fail on windows-latest CI lane only; macOS/Linux CI green so defect ships undetected", + "line": 737 + }, + { + "id": "DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.detect", + "klass": "DEFECT", + "value": "grep tests for \\`.mode & 0o777\\` / \\`.mode) === 0o\\` / \\`writeFileSync(...{ mode: 0o\\` / \\`chmodSync\\` paired with a strict-equality assertion on the resulting mode; any such assertion is a POSIX-only fact that will diverge on Windows (write reads back as 0o666)", + "line": 733 + }, + { + "id": "DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.examples", + "klass": "DEFECT", + "value": "#1634/PR #1638 tests/capability-lifecycle.test.cjs \"a .cjs hook command is node-prefixed so it runs without the executable bit\" failed windows-latest,24 on \"precondition: file staged without +x\" (expected 420/0o644, got 438/0o666); the node-prefix behavioral assertion was correct — only the mode-bit precondition was the POSIX-only fact", + "line": 732 + }, + { + "id": "DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.fix-forward", + "klass": "DEFECT", + "value": "gate the mode-bit precondition on if (process.platform !== 'win32') — the executable-bit/mode is a POSIX concept meaningless on Windows; KEEP the platform-independent behavioral assertion (the actual behavior under test) running on every OS; do NOT delete the precondition, scope it to POSIX", + "line": 734 + }, + { + "id": "DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.prevention", + "klass": "DEFECT", + "value": "ref DEFECT.WINDOWS-TEST-PORTABILITY — gsd-test is Mac/Linux only (no Windows host), only the CI windows-latest lane catches this; run npm run lint:ci (lint-windows-test-portability) before push; prefer asserting the BEHAVIOR (command shape, runnability) over the filesystem mode bit", + "line": 735 + }, + { + "id": "DEFECT.WINDOWS-POSIX-MODE-BIT-ASSERT.symptom", + "klass": "DEFECT", + "value": "a test writes a file with a POSIX mode (fs.writeFileSync(p, data, {mode: 0o644}) or fs.chmodSync) then asserts fs.statSync(p).mode & 0o777 === ; passes on macOS/Linux/ubuntu CI, FAILS on the windows-latest CI lane — Windows fs does NOT honor POSIX write modes, Node reports the mode derived from the DOS readonly attribute (0o666 for writable / 0o444 for readonly), never the requested 0o644/0o755", + "line": 731 + }, + { + "id": "DEFECT.WINDOWS-TEST-PORTABILITY.detect", + "klass": "DEFECT", + "value": "npm run lint:windows-test-portability (tripwire: flags tests combining chmod exec-bit with sh/bash -c and no platform guard); watch CI windows matrix green before declaring a PR done", + "line": 727 + }, + { + "id": "DEFECT.WINDOWS-TEST-PORTABILITY.examples", + "klass": "DEFECT", + "value": "PR #1084 (chmod 0o755 + bare-command execution failed on windows lane); test files that assert path.join result without normalizing to forward slashes", + "line": 726 + }, + { + "id": "DEFECT.WINDOWS-TEST-PORTABILITY.fix-forward", + "klass": "DEFECT", + "value": "gate platform-specific execution with if (process.platform !== 'win32'); normalize path expectations to forward slashes with .replace(/\\\\/g, '/'); invoke scripts via explicit interpreter (sh ) rather than relying on exec-bit; annotate // windows-portability-ok: when a bypass is intentional", + "line": 728 + }, + { + "id": "DEFECT.WINDOWS-TEST-PORTABILITY.prevention", + "klass": "DEFECT", + "value": "run lint:ci before opening a PR; treat the CI windows lane as the only true Windows signal — gsd-test (Mac/Linux only) cannot substitute for it", + "line": 729 + }, + { + "id": "DEFECT.WINDOWS-TEST-PORTABILITY.symptom", + "klass": "DEFECT", + "value": "local gsd-test runs Mac+Linux only (no Windows host); Windows-only test failures (chmod exec-bit not honored for PATH-executing extension-less scripts in Git Bash msys2; / vs \\ path-separator in assertions; Git Bash msys2 shell semantics) surface ONLY in CI test (windows-latest,*) / full test (windows-latest,*) lanes, never locally", + "line": 725 + }, + { + "id": "DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.detect", + "klass": "DEFECT", + "value": "after install, for every workflow .md file under //workflows/, extract the @ reference from the body and assert fs.existsSync(path); if any reference target is absent, this defect is present", + "line": 753 + }, + { + "id": "DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.examples", + "klass": "DEFECT", + "value": "PR #1622 (issue #1615) shipped Windsurf /gsd-* workflow wrappers that all reference /.windsurf/gsd-core/commands/gsd/X.md; that directory was never populated; none of the reviews (security, Codex adversarial, Memtrace) caught it; a #1629 regression test verifying 'every workflow @- reference target exists on disk' surfaced it post-merge", + "line": 752 + }, + { + "id": "DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.fix-forward", + "klass": "DEFECT", + "value": "copy the canonical command source (commands/gsd/*.md) into /gsd-core/commands/gsd/ during install, gated on the runtime that uses workflow delegation (currently Windsurf local only); use copyWithPathReplacement to apply the same path+brand rewrites as the rest of the install; verify with a regression test that every workflow's @-reference resolves", + "line": 754 + }, + { + "id": "DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.prevention", + "klass": "DEFECT", + "value": "any new converter that emits a wrapper file delegating to another file MUST verify the delegation target is actually written by the same install; add a post-install invariant test: for every @ reference in every generated wrapper, assert the target exists; the workflow converter's hardcoded path was copy-pasted from Claude's skill pattern without verifying the target exists for the new runtime", + "line": 755 + }, + { + "id": "DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.symptom", + "klass": "DEFECT", + "value": "workflow wrapper file (e.g. Windsurf convertClaudeCommandToWindsurfWorkflow) delegates to a command body at /gsd-core/commands/gsd/X.md via a hardcoded @~/.claude/gsd-core/commands/gsd/ path that _applyRuntimeRewrites rewrites to the install target; the source gsd-core/ dir ships without commands/ (it lives at package-root commands/gsd/); install completes successfully, workflow files appear in the / menu, but invocation tells the LLM to read a file that does not exist; the slash commands silently fail", + "line": 751 + }, + { + "id": "DEFECT.WORKTREE-FETCH-SHA-DIVERGENCE.detect", + "klass": "DEFECT", + "value": "git rev-parse HEAD~1 vs git rev-parse origin/ — if they differ despite fetch the local copy was rewritten by some checkout-time hook", + "line": 675 + }, + { + "id": "DEFECT.WORKTREE-FETCH-SHA-DIVERGENCE.examples", + "klass": "DEFECT", + "value": "this session, branch fix/3309-... and pr-3316", + "line": 674 + }, + { + "id": "DEFECT.WORKTREE-FETCH-SHA-DIVERGENCE.fix-forward", + "klass": "DEFECT", + "value": "git checkout --detach origin/ directly; do work from detached HEAD; push HEAD:", + "line": 676 + }, + { + "id": "DEFECT.WORKTREE-FETCH-SHA-DIVERGENCE.symptom", + "klass": "DEFECT", + "value": "in a worktree, git fetch origin pull/N/head:pr-N produces commits with SHAs different from the actual remote PR head SHA; force-push rejected as non-fast-forward despite recent fetch", + "line": 673 + }, + { + "id": "EXEC.CLASSIFY.classes", + "klass": "EXEC", + "value": "{class:'quota-exceeded'|'classify-handoff-bug'|'unknown-failure', sentinel?, retryAfterSeconds?}", + "line": 839 + }, + { + "id": "EXEC.CLASSIFY.cross-runtime", + "klass": "EXEC", + "value": "Anthropic/CC: usage limit|rate limit|quota|429|retry-after; Copilot CLI: rate_limit (stem); Codex CLI: 429|usage_limit_reached|too many requests; Gemini CLI: RESOURCE_EXHAUSTED|exceeded your", + "line": 841 + }, + { + "id": "EXEC.CLASSIFY.handler", + "klass": "EXEC", + "value": "gsd-core/bin/lib/agent-command-router.cjs:classifyAgentFailure (registered via command-aliases.cjs; mutation:false outputMode:json)", + "line": 837 + }, + { + "id": "EXEC.CLASSIFY.precedence", + "klass": "EXEC", + "value": "quota sentinel wins over classifyHandoffIfNeeded bug when both appear", + "line": 842 + }, + { + "id": "EXEC.CLASSIFY.proactive-signal-not-usable", + "klass": "EXEC", + "value": "Anthropic exposes anthropic-ratelimit-* headers + Agent SDK RateLimitEvent; Claude Code subprocess does NOT forward to hooks/statusline today (upstream #33820, #22407, #32796)", + "line": 844 + }, + { + "id": "EXEC.CLASSIFY.retry-after-parser", + "klass": "EXEC", + "value": "\\bretry[-_ ]after[:\\s]+(\\d+)\\b avoids embedded-word false matches like noretry-after", + "line": 843 + }, + { + "id": "EXEC.CLASSIFY.sentinel-order", + "klass": "EXEC", + "value": "most specific first: 429 beats too-many-requests; quota beats resource_exhausted; case-insensitive; canonical sentinel value is lower-cased form", + "line": 840 + }, + { + "id": "EXEC.CLASSIFY.workflow", + "klass": "EXEC", + "value": "gsd-core/workflows/execute-phase.md step 7; class-distinct prompts (quota-to-wait-for-reset; classify-handoff-bug-to-spot-check; unknown-to-continue/stop)", + "line": 838 + }, + { + "id": "GSD-RESEARCH.CONTEXT-DISCIPLINE", + "klass": "GSD-RESEARCH", + "value": "less-context levers: subagent isolation + compact provider output + fetches-to-disk + cache-returns-digest; API clear_tool_uses/memory tool are the conceptual model, not a Claude Code harness knob", + "line": 284 + }, + { + "id": "GSD-RESEARCH.INTEGRATION.L2-hybrid", + "klass": "GSD-RESEARCH", + "value": "code owns cache+legitimacy+confidence+provider-pick (gsd-tools query research-plan/research-store/package-legitimacy); MCP owns the fetch; agent returns RESEARCH.md path, never raw fetches", + "line": 282 + }, + { + "id": "GSD-RESEARCH.MODULE.package-legitimacy", + "klass": "GSD-RESEARCH", + "value": "registry-API verdicts (npm/PyPI/crates.io injectable adapters) computed from thresholds {minAgeDays:30,minWeeklyDownloads:1000,requireRepo:true}; verdict OK|SUS|SLOP per package; slopcheck=optional adapter that can only escalate, never the install-or-degrade gate", + "line": 281 + }, + { + "id": "GSD-RESEARCH.MODULE.research-provider", + "klass": "GSD-RESEARCH", + "value": "single source of truth PROVIDER_WATERFALL (docs Context7->Ref->Jina->websearch; web Exa->Tavily->Perplexity->Brave->websearch; scrape Firecrawl->Jina); planResearch returns cache-hits+fetch-plan; classifyConfidence stamps HIGH|MEDIUM|LOW by provider AUTHORITY + verification EVIDENCE (HIGH requires code-computed ground-truth corroboration e.g. legitimacyVerdict OK; provider authority alone caps at MEDIUM; SLOP caps at LOW); Firecrawl is scrape-only (not in docs/web discovery)", + "line": 280 + }, + { + "id": "GSD-RESEARCH.MODULE.research-store", + "klass": "GSD-RESEARCH", + "value": "content-addressed cache; key=sha256(ecosystem+library+version+query+kind); getResearch->{hit,stale} never throws (mirrors graphify staleness); ttlForSource curated HIGH 30d|MED 7d|web LOW 1d; tiers: curated-doc kinds -> ~/.gsd/research-cache (cross-project), web/synthesis -> project .planning/research/.cache", + "line": 279 + }, + { + "id": "GSD-RESEARCH.PROVIDER.availability", + "klass": "GSD-RESEARCH", + "value": "config flags brave_search/exa_search/firecrawl/tavily_search/ref_search/perplexity/jina (env _API_KEY or ~/.gsd/_api_key); context7/jina/websearch always available; planResearch falls through waterfall to websearch terminal", + "line": 283 + }, + { + "id": "LEARNING.prompt-budget.boundary-gap", + "klass": "LEARNING", + "value": "PR #3708 commit 2df566ed reserved NOTE_RESERVE_TOKENS in pressure-threshold AND in minSet pre-check; both buggy paths only fire when baseTokens ∈ (effectiveBudget - NOTE_RESERVE_TOKENS, effectiveBudget]; original test suite used budgets far from that band so neither path was exercised; fix bde1ae8f confines NOTE_RESERVE accounting to post-trim assembly path only; future budget/limit code MUST add boundary fixtures per RULESET.TESTS.boundary-coverage.fixtures", + "line": 373 + }, + { + "id": "META.RULE.brief-must-cite-doc", + "klass": "META", + "value": "agent prompts MUST quote the canonical doc line being applied; paraphrasing from predicate memory drifts and produces violations", + "line": 514 + }, + { + "id": "META.RULE.brief-no-paraphrase", + "klass": "META", + "value": "writing \"k040 — never leave changelog box unchecked\" caused 5 of 8 agents to edit CHANGELOG.md in violation of CONTRIBUTING.md L110", + "line": 515 + }, + { + "id": "META.RULE.canonical-source-precedence", + "klass": "META", + "value": "CONTRIBUTING.md > docs/adr/* > CONTEXT.md > agent memory", + "line": 512 + }, + { + "id": "META.RULE.read-contributing-first", + "klass": "META", + "value": "read CONTRIBUTING.md sections \"Pull Request Guidelines\" + \"CHANGELOG Entries\" before EVERY agent dispatch", + "line": 513 + }, + { + "id": "PLANNING.PATH.PARITY.project-scope", + "klass": "PLANNING", + "value": ".planning/ (never .planning/projects/); mirror planning-workspace.cjs planningDir()", + "line": 458 + }, + { + "id": "PLANNING.PATH.SEAM.helpers", + "klass": "PLANNING", + "value": "helpers.planningPaths delegates to workspacePlanningPaths + resolveWorkspaceContext; precedence explicit-ws > env-ws > env-project > root", + "line": 459 + }, + { + "id": "PLANNING.PATH.SEAM.init-handlers", + "klass": "PLANNING", + "value": "[initExecutePhase, initPlanPhase, initPhaseOp, initMilestoneOp] consume helpers.planningPaths().planning (no direct relPlanningPath join)", + "line": 460 + }, + { + "id": "PR.3267.POSTMORTEM.recovery", + "klass": "PR", + "value": "[issue#3270 created, label approved-enhancement applied, PR reopened, body includes \"Closes #3270\", label no-changelog applied]", + "line": 436 + }, + { + "id": "PR.3267.POSTMORTEM.root-cause", + "klass": "PR", + "value": "[missing issue link, missing changeset/no-changelog]", + "line": 435 + }, + { + "id": "PRED.k320.canonical-source", + "klass": "PRED", + "value": "CONTRIBUTING.md L110-123", + "line": 518 + }, + { + "id": "PRED.k320.ci-enforcement", + "klass": "PRED", + "value": "scripts/changeset/lint.cjs", + "line": 524 + }, + { + "id": "PRED.k320.ci-paths-monitored", + "klass": "PRED", + "value": "bin/ gsd-core/ agents/ commands/ docs/ hooks/ tests/ scripts/", + "line": 525 + }, + { + "id": "PRED.k320.cure", + "klass": "PRED", + "value": "drop .changeset/--.md fragment ONLY", + "line": 520 + }, + { + "id": "PRED.k320.evidence", + "klass": "PRED", + "value": "PR #3302 merge-conflict against #3308 CHANGELOG.md row 2026-05-09", + "line": 527 + }, + { + "id": "PRED.k320.opt-out-label", + "klass": "PRED", + "value": "no-changelog", + "line": 523 + }, + { + "id": "PRED.k320.recovery", + "klass": "PRED", + "value": "open Removed-typed cleanup PR deleting only the redundant row", + "line": 526 + }, + { + "id": "PRED.k320.rule", + "klass": "PRED", + "value": "do not edit CHANGELOG.md in feature/fix/enhancement PRs", + "line": 519 + }, + { + "id": "PRED.k320.signal", + "klass": "PRED", + "value": "changelog-direct-edit-forbidden", + "line": 517 + }, + { + "id": "PRED.k320.tool", + "klass": "PRED", + "value": "npm run changeset -- --type --pr --body \"...\"", + "line": 521 + }, + { + "id": "PRED.k320.types", + "klass": "PRED", + "value": "Added|Changed|Deprecated|Removed|Fixed|Security", + "line": 522 + }, + { + "id": "PRED.k321.evidence", + "klass": "PRED", + "value": "PRs #3304/#3305 (2026-05-09): real Minor/Major findings in body, 0 threads", + "line": 533 + }, + { + "id": "PRED.k321.poll-shape", + "klass": "PRED", + "value": "parse pulls//reviews body AND graphql reviewThreads", + "line": 531 + }, + { + "id": "PRED.k321.resolution", + "klass": "PRED", + "value": "address in code; no GraphQL resolveReviewThread needed for body-only findings", + "line": 532 + }, + { + "id": "PRED.k321.shape", + "klass": "PRED", + "value": "CR posts \"[!CAUTION] outside the diff\" findings in review BODY, not in reviewThreads", + "line": 530 + }, + { + "id": "PRED.k321.signal", + "klass": "PRED", + "value": "cr-outside-diff-range-finding", + "line": 529 + }, + { + "id": "PRED.k322.cure-1", + "klass": "PRED", + "value": "2nd retrigger ~10min after first ack", + "line": 538 + }, + { + "id": "PRED.k322.cure-2", + "klass": "PRED", + "value": "if silent at 50min, treat as silent-pass with maintainer flag in merge-commit body", + "line": 539 + }, + { + "id": "PRED.k322.distinct-from", + "klass": "PRED", + "value": "k080", + "line": 536 + }, + { + "id": "PRED.k322.evidence", + "klass": "PRED", + "value": "PR #3306 (2026-05-09): 0 reviews after 50min + 2 retriggers", + "line": 541 + }, + { + "id": "PRED.k322.merge-gate-impact", + "klass": "PRED", + "value": "k070 real_coderabbit_review_present unsatisfied; requires maintainer judgment", + "line": 540 + }, + { + "id": "PRED.k322.shape", + "klass": "PRED", + "value": "ack posted, real review never lands within [5s, 410s] cooldown after burst of N PRs <15min", + "line": 537 + }, + { + "id": "PRED.k322.signal", + "klass": "PRED", + "value": "cr-sustained-throttle", + "line": 535 + }, + { + "id": "PRED.k323.cure-alt", + "klass": "PRED", + "value": "consolidate into single PR when 2+ issues share root cause", + "line": 546 + }, + { + "id": "PRED.k323.cure-pre-dispatch", + "klass": "PRED", + "value": "brief one agent canonical-owner; brief others to EXCLUDE shared site", + "line": 545 + }, + { + "id": "PRED.k323.evidence", + "klass": "PRED", + "value": "#3300 (#3297) overlapped #3306 (#3298) on add-backlog.md hunks 2026-05-09", + "line": 548 + }, + { + "id": "PRED.k323.recovery", + "klass": "PRED", + "value": "close smaller PR as \"subsumed by #N\" or rebase second to drop overlap hunk", + "line": 547 + }, + { + "id": "PRED.k323.shape", + "klass": "PRED", + "value": "2+ open issues touch same canonical bug site; each fix's sibling-audit produces overlapping diff", + "line": 544 + }, + { + "id": "PRED.k323.signal", + "klass": "PRED", + "value": "sibling-audit-cross-pr-overlap", + "line": 543 + }, + { + "id": "PRED.k324.cure", + "klass": "PRED", + "value": "verify via gh api on every agent-completion notification; never trust narrative", + "line": 552 + }, + { + "id": "PRED.k324.evidence", + "klass": "PRED", + "value": "2026-05-09 session: 5+ mid-monitor terminations across PRs #3232/#3271/#3251/#3255/#3262", + "line": 554 + }, + { + "id": "PRED.k324.k095-restatement", + "klass": "PRED", + "value": "k095 confirmed shape: agent reports \"waiting for monitor\" / \"tests still running\" then terminates", + "line": 551 + }, + { + "id": "PRED.k324.poll-shape", + "klass": "PRED", + "value": "gh pr view --json mergeStateStatus,statusCheckRollup + pulls//reviews + graphql reviewThreads + issues//comments tail", + "line": 553 + }, + { + "id": "PRED.k324.signal", + "klass": "PRED", + "value": "agent-terminates-mid-monitor", + "line": 550 + }, + { + "id": "PRED.k325.cleanup", + "klass": "PRED", + "value": "git worktree remove --force for aged agent worktrees", + "line": 559 + }, + { + "id": "PRED.k325.cure", + "klass": "PRED", + "value": "detached-HEAD: git checkout --detach $(git ls-remote origin ); modify; commit; git push --force-with-lease=: origin HEAD:refs/heads/", + "line": 558 + }, + { + "id": "PRED.k325.evidence", + "klass": "PRED", + "value": "2026-05-09 CHANGELOG.md strip on PRs #3300/#3302/#3304/#3305 required detached-HEAD", + "line": 560 + }, + { + "id": "PRED.k325.shape", + "klass": "PRED", + "value": "git checkout errors \"already used by worktree at \"", + "line": 557 + }, + { + "id": "PRED.k325.signal", + "klass": "PRED", + "value": "worktree-branch-lock-on-force-push", + "line": 556 + }, + { + "id": "PRED.k326.cure", + "klass": "PRED", + "value": "quote canonical doc verbatim in brief; mentally simulate \"if all N agents follow this brief literally, do they violate any rule?\"", + "line": 564 + }, + { + "id": "PRED.k326.evidence", + "klass": "PRED", + "value": "2026-05-09 brief \"k040 — update CHANGELOG.md\" → 5 of 8 agents violated CONTRIBUTING.md L110", + "line": 565 + }, + { + "id": "PRED.k326.shape", + "klass": "PRED", + "value": "N parallel agents amplify a single brief-vs-doc contradiction into N violations", + "line": 563 + }, + { + "id": "PRED.k326.signal", + "klass": "PRED", + "value": "brief-contradicts-canonical-doc", + "line": 562 + }, + { + "id": "PRED.k327.ack-shape", + "klass": "PRED", + "value": "body \"✅ Actions performed - Full review triggered\"", + "line": 568 + }, + { + "id": "PRED.k327.cooldown-normal", + "klass": "PRED", + "value": "[5s, 410s]", + "line": 571 + }, + { + "id": "PRED.k327.cooldown-throttled", + "klass": "PRED", + "value": "k322", + "line": 572 + }, + { + "id": "PRED.k327.distinguish-key", + "klass": "PRED", + "value": "len(pulls//reviews) — ack=0, real=≥1", + "line": 570 + }, + { + "id": "PRED.k327.real-review-shape", + "klass": "PRED", + "value": "body starts \"Actionable comments posted: N\" OR \"[!CAUTION] Some comments are outside the diff\"", + "line": 569 + }, + { + "id": "PRED.k327.signal", + "klass": "PRED", + "value": "cr-ack-vs-real-review", + "line": 567 + }, + { + "id": "PRED.k328.audit-list", + "klass": "PRED", + "value": "[heading-matches-class, closing-keyword-present, changeset-fragment-or-no-changelog-label]", + "line": 577 + }, + { + "id": "PRED.k328.canonical-source", + "klass": "PRED", + "value": "CONTRIBUTING.md L101", + "line": 575 + }, + { + "id": "PRED.k328.k100-restatement", + "klass": "PRED", + "value": "heading must match issue class: bug→## Fix PR, enhancement→## Enhancement PR, feature→## Feature PR", + "line": 576 + }, + { + "id": "PRED.k328.signal", + "klass": "PRED", + "value": "pr-template-typed-heading-required", + "line": 574 + }, + { + "id": "PRED.k329.body", + "klass": "PRED", + "value": "**** — . (#)", + "line": 583 + }, + { + "id": "PRED.k329.canonical-source", + "klass": "PRED", + "value": "CONTRIBUTING.md L112-117 + .changeset/README.md", + "line": 580 + }, + { + "id": "PRED.k329.filename", + "klass": "PRED", + "value": ".changeset/--.md", + "line": 581 + }, + { + "id": "PRED.k329.frontmatter", + "klass": "PRED", + "value": "---\\\\ntype: \\\\npr: \\\\n---", + "line": 582 + }, + { + "id": "PRED.k329.observed-clean", + "klass": "PRED", + "value": "#3299 sunny-ibex-wave, #3301 sturdy-rams-caper, #3306 3298-phase-dir-prefix-drift-workflows", + "line": 584 + }, + { + "id": "PRED.k329.signal", + "klass": "PRED", + "value": "changeset-fragment-canonical-shape", + "line": 579 + }, + { + "id": "PRED.k330.fallback", + "klass": "PRED", + "value": "append predicate-format findings directly to CONTEXT.md", + "line": 588 + }, + { + "id": "PRED.k330.shape", + "klass": "PRED", + "value": "mempalace MCP tools require explicit user call; AI cannot trigger", + "line": 587 + }, + { + "id": "PRED.k330.signal", + "klass": "PRED", + "value": "mempalace-diary-not-callable-by-ai", + "line": 586 + }, + { + "id": "PRED.k331.cure", + "klass": "PRED", + "value": "gh pr close with NO --comment flag", + "line": 593 + }, + { + "id": "PRED.k331.evidence", + "klass": "PRED", + "value": "2026-05-09 wave-3: violation on #3300 close, deleted within 30s", + "line": 595 + }, + { + "id": "PRED.k331.k101-restatement", + "klass": "PRED", + "value": "k101 includes close-time --comment flag; rationale belongs in subsuming PR's squash-merge body", + "line": 592 + }, + { + "id": "PRED.k331.recovery", + "klass": "PRED", + "value": "if violation lands, gh api -X DELETE repos///issues/comments/", + "line": 594 + }, + { + "id": "PRED.k331.shape", + "klass": "PRED", + "value": "instruction \"close with no comment (rationale)\" — parenthetical is rationale, NOT comment body", + "line": 591 + }, + { + "id": "PRED.k331.signal", + "klass": "PRED", + "value": "close-with-no-comment-is-literal", + "line": 590 + }, + { + "id": "PROC.AGENT-DISPATCH.completion-verify", + "klass": "PROC", + "value": "run k324.poll-shape on every agent-completion notification", + "line": 599 + }, + { + "id": "PROC.AGENT-DISPATCH.parallel-overlap-audit", + "klass": "PROC", + "value": "before dispatching N sibling-audit fixers, compute file-set union and assign canonical owners", + "line": 598 + }, + { + "id": "PROC.AGENT-DISPATCH.preflight", + "klass": "PROC", + "value": "[read-CONTRIBUTING.md-fresh, read-relevant-ADRs, cite-specific-line-in-brief, require-closing-keyword, require-changeset-fragment, forbid-CHANGELOG.md-edit, require-isolation-worktree, forbid-self-PR-comment, mandate-trust-but-verify]", + "line": 597 + }, + { + "id": "PROC.MERGE-WAVE.changelog-strip-pattern", + "klass": "PROC", + "value": "detached-HEAD per k325 + git checkout main -- CHANGELOG.md + commit + force-with-lease", + "line": 603 + }, + { + "id": "PROC.MERGE-WAVE.merge-tool", + "klass": "PROC", + "value": "gh pr merge --squash --delete-branch", + "line": 604 + }, + { + "id": "PROC.MERGE-WAVE.merge-tool-warning", + "klass": "PROC", + "value": "delete-branch may fail with \"used by worktree at\" — harmless; remote branch still deleted", + "line": 605 + }, + { + "id": "PROC.MERGE-WAVE.ordering", + "klass": "PROC", + "value": "[wave1: isolated-files, wave2: CHANGELOG-only-overlap (better: strip per k320), wave3: same-file-overlap with explicit decision]", + "line": 601 + }, + { + "id": "PROC.MERGE-WAVE.preflight", + "klass": "PROC", + "value": "gh pr view --json files for every PR; identify overlap pairs; surface to maintainer", + "line": 602 + }, + { + "id": "PROC.PARALLEL-FIX-DISPATCH.observed", + "klass": "PROC", + "value": "#3541 + #3542 dispatched simultaneously this session; PRs #3546 #3547 opened green; one syntax slip caught by AGENT-RETIRED-SLASH-SYNTAX-DRIFT and fixed before second PR opened", + "line": 869 + }, + { + "id": "PROC.PARALLEL-FIX-DISPATCH.pattern", + "klass": "PROC", + "value": "bot triage brief → worktree per branch → parallel sub-agents do rubber-duck/RCA/TDD implementation only → top-level orchestrator owns commit + gsd-test-summary --both + push + PR + changeset-pr-backfill", + "line": 867 + }, + { + "id": "PROC.PARALLEL-FIX-DISPATCH.rationale", + "klass": "PROC", + "value": "long-running test runs need cross-turn notifications (orchestrator-only); CONTRIBUTING.md gh-templates-first hook requires session-scoped Read calls sub-agents wouldn't otherwise make; sequencing test runs avoids GSD-TEST-CONCURRENT-OUTPUT-COLLISION", + "line": 868 + }, + { + "id": "PROC.TRIAGE.comment-shape", + "klass": "PROC", + "value": "lead with \"duplicate of #NNNN, fixed by PR #MMMM, in v1.X.Y\"; show current code snippet proving bug-surface gone; give @latest and @next upgrade commands; close", + "line": 874 + }, + { + "id": "PROC.TRIAGE.no-duplicate-label", + "klass": "PROC", + "value": "this repo has no duplicate label; framing lives in comment text + closing the issue", + "line": 875 + }, + { + "id": "PROC.TRIAGE.routing-incoming", + "klass": "PROC", + "value": "stale-bug-already-fixed to close as duplicate of originating issue + cite fix PR + first stable tag; release-publish-or-backport to ready-for-human; reporter-can-self-test to awaiting-retest", + "line": 873 + }, + { + "id": "RELEASE-NOTES.ANTI-PATTERN", + "klass": "RELEASE-NOTES", + "value": "raw \"What's Changed\" PR list as final body for hotfix or feature release; \"Full Changelog only\" body for tagged release with >0 user-facing fixes", + "line": 494 + }, + { + "id": "RELEASE-NOTES.ANTI-PATTERN.implementation-first", + "klass": "RELEASE-NOTES", + "value": "do not lead bullet with file path or function name; lead with symptom/user-visible behavior", + "line": 495 + }, + { + "id": "RELEASE-NOTES.ANTI-PATTERN.risk-commentary", + "klass": "RELEASE-NOTES", + "value": "do not include \"may break\", \"be careful\", \"test thoroughly\" - per global CLAUDE.md no-risk-commentary rule", + "line": 496 + }, + { + "id": "RELEASE-NOTES.DEFAULT-STATE", + "klass": "RELEASE-NOTES", + "value": "auto-generated body is \"What's Changed\" PR list + Full Changelog link; treat as draft, not final", + "line": 470 + }, + { + "id": "RELEASE-NOTES.EXAMPLE.hotfix", + "klass": "RELEASE-NOTES", + "value": "v1.41.1 (https://github.com/open-gsd/gsd-core/releases/tag/v1.41.1) - 14 fixes grouped by 6 subgroups", + "line": 498 + }, + { + "id": "RELEASE-NOTES.EXAMPLE.minor-auto-acceptable", + "klass": "RELEASE-NOTES", + "value": "v1.41.0 - kept auto-generated body; many small fixes with clean conventional-commit titles", + "line": 500 + }, + { + "id": "RELEASE-NOTES.EXAMPLE.rc", + "klass": "RELEASE-NOTES", + "value": "v1.42.0-rc1 (https://github.com/open-gsd/gsd-core/releases/tag/v1.42.0-rc1) - intro + Added/Changed/Fixed/Documentation taxonomy", + "line": 499 + }, + { + "id": "RELEASE-NOTES.GATE.hotfix", + "klass": "RELEASE-NOTES", + "value": "manual edit required; auto-generated body for vX.Y.{Z>0} is \"Full Changelog only\" and must be replaced with structured body", + "line": 471 + }, + { + "id": "RELEASE-NOTES.GATE.minor", + "klass": "RELEASE-NOTES", + "value": "auto-generated body acceptable when PR titles are clean; promote to structured body when >20 PRs or contains feature+refactor+fix mix", + "line": 473 + }, + { + "id": "RELEASE-NOTES.GATE.rc", + "klass": "RELEASE-NOTES", + "value": "manual edit recommended; auto-generated PR list is acceptable for early RCs but final RC before vX.Y.0 should match standard", + "line": 472 + }, + { + "id": "RELEASE-NOTES.RELEASE-STREAM.main-branch", + "klass": "RELEASE-NOTES", + "value": "next (RCs) + latest (stable); install via @next or @latest", + "line": 505 + }, + { + "id": "RELEASE-NOTES.RELEASE-STREAM.rule", + "klass": "RELEASE-NOTES", + "value": "streams do not mix; do not document @next in hotfix/stable notes", + "line": 506 + }, + { + "id": "RELEASE-NOTES.SCOPE", + "klass": "RELEASE-NOTES", + "value": "GitHub Releases body for tags vX.Y.Z, vX.Y.Z-rcN; not CHANGELOG.md (changeset workflow owns that)", + "line": 469 + }, + { + "id": "RELEASE-NOTES.SOURCE.changesets", + "klass": "RELEASE-NOTES", + "value": ".changeset/*.md (frontmatter pr: + body bullets)", + "line": 485 + }, + { + "id": "RELEASE-NOTES.SOURCE.commits", + "klass": "RELEASE-NOTES", + "value": "git log .. --pretty=format:'%s%n%n%b' --no-merges", + "line": 484 + }, + { + "id": "RELEASE-NOTES.SOURCE.pr-bodies", + "klass": "RELEASE-NOTES", + "value": "gh pr view --json title,body for fixes lacking a changeset", + "line": 486 + }, + { + "id": "RELEASE-NOTES.SOURCE.precedence", + "klass": "RELEASE-NOTES", + "value": "changeset body > commit body > PR body > commit subject (prefer authored content over auto-generated)", + "line": 487 + }, + { + "id": "RELEASE-NOTES.STANDARD.bullet-shape", + "klass": "RELEASE-NOTES", + "value": "**Bold user-visible change** — explanation of what was broken or what's new, leading with symptom not implementation. Trailing (#NNN) PR ref.", + "line": 477 + }, + { + "id": "RELEASE-NOTES.STANDARD.footer.full-changelog", + "klass": "RELEASE-NOTES", + "value": "**Full Changelog**: https://github.com/open-gsd/gsd-core/compare/...", + "line": 481 + }, + { + "id": "RELEASE-NOTES.STANDARD.footer.hotfix", + "klass": "RELEASE-NOTES", + "value": "Install/upgrade: \\`npx @opengsd/gsd-core@latest\\`", + "line": 479 + }, + { + "id": "RELEASE-NOTES.STANDARD.footer.rc", + "klass": "RELEASE-NOTES", + "value": "Install for testing: \\`npx @opengsd/gsd-core@next\\` (per branch->dist-tag policy)", + "line": 480 + }, + { + "id": "RELEASE-NOTES.STANDARD.heading-level", + "klass": "RELEASE-NOTES", + "value": "## for category, ### for subgroup (area), - for bullet", + "line": 476 + }, + { + "id": "RELEASE-NOTES.STANDARD.intro", + "klass": "RELEASE-NOTES", + "value": "optional one-paragraph framing for RC/feature releases; omit for pure-fix hotfixes", + "line": 482 + }, + { + "id": "RELEASE-NOTES.STANDARD.subgroups", + "klass": "RELEASE-NOTES", + "value": "phase-planning-state | workstream | query-dispatch-cli | code-review | install | capture | docs | architecture | security", + "line": 478 + }, + { + "id": "RELEASE-NOTES.STANDARD.taxonomy", + "klass": "RELEASE-NOTES", + "value": "Keep-a-Changelog 1.1.0: Added | Changed | Deprecated | Removed | Fixed | Security | Documentation", + "line": 475 + }, + { + "id": "RELEASE-NOTES.TEMPLATE.hotfix", + "klass": "RELEASE-NOTES", + "value": "## Fixed\\n\\n### \\n- **** — . (#)\\n\\n---\\n\\nInstall/upgrade: \\`npx @opengsd/gsd-core@latest\\`\\n\\n**Full Changelog**: ", + "line": 502 + }, + { + "id": "RELEASE-NOTES.TEMPLATE.rc", + "klass": "RELEASE-NOTES", + "value": "\\n\\n## Added\\n### \\n- **** — . (#)\\n\\n## Changed\\n### Architecture\\n- **** — . (#)\\n\\n## Fixed\\n### \\n- **** — . (#)\\n\\n## Documentation\\n- **** — . (#)\\n\\n---\\n\\nThis is a release candidate. Install for testing:\\n\\`\\`\\`bash\\nnpx @opengsd/gsd-core@next\\n\\`\\`\\`\\n\\n**Full Changelog**: ", + "line": 503 + }, + { + "id": "RELEASE-NOTES.WORKFLOW.edit", + "klass": "RELEASE-NOTES", + "value": "gh release edit --notes-file ", + "line": 489 + }, + { + "id": "RELEASE-NOTES.WORKFLOW.idempotency", + "klass": "RELEASE-NOTES", + "value": "gh release edit overwrites body wholesale; safe to re-run after refining", + "line": 492 + }, + { + "id": "RELEASE-NOTES.WORKFLOW.token", + "klass": "RELEASE-NOTES", + "value": "must use .envrc GITHUB_TOKEN per project CLAUDE.md; never ambient gh auth", + "line": 491 + }, + { + "id": "RELEASE-NOTES.WORKFLOW.view", + "klass": "RELEASE-NOTES", + "value": "gh release view --json body --jq .body", + "line": 490 + }, + { + "id": "RULESET.ADR-HEADER", + "klass": "RULESET", + "value": "every docs/adr/NNNN-*.md must open with - **Status:** Accepted|Proposed|Deprecated + - **Date:** YYYY-MM-DD immediately after title", + "line": 399 + }, + { + "id": "RULESET.AGENT_SIZE_BUDGET", + "klass": "RULESET", + "value": "agent-size-budget (#1074; sibling of WORKFLOW_SIZE_BUDGET; BYTES not lines per #717/#683, rebased from lines in PR 3/3) = per-file baseline (PRIMARY anti-creep: tests/agent-size-baseline.json pins each agents/gsd-*.md exact byte size) + loose tier hard caps (red lines, never raised on approach: XL<=57344 / LARGE<=49152 / DEFAULT<=24576); net-new agents are DEFAULT-tier (no separate new-file cap). One 'npm run size:baseline' regenerates BOTH workflow and agent baselines via the shared scripts/workflow-size.cjs measureMdFiles(dir,predicate) counter. A grown agent fails the baseline guard — regenerate + justify, or extract LAZILY to gsd-core/references/. DISTINCT from DEFECT.AGENT-FILE-SIZE-CAP-BREACH (a separate 45K-CHAR extraction-evidence threshold on gsd-planner via planner-decomposition/reachability tests): that guard proves mode-sections were extracted; this one bounds total agent bytes. Two guards, two units (chars vs bytes), two purposes", + "line": 386 + }, + { + "id": "RULESET.ALLOWED-TOOLS-FRONTMATTER", + "klass": "RULESET", + "value": "command's allowed-tools must cover every tool the workflow calls (including Write for file creation); thin-wrapper pattern makes this easy to miss", + "line": 392 + }, + { + "id": "RULESET.ARGUMENTS-SANITIZE", + "klass": "RULESET", + "value": "any workflow step constructing .planning/.../{SLUG}.md path from user input ($ARGUMENTS, parsed remainder) must sanitize inline ([a-z0-9-] only, reject ..//\\\\, max-length) — \"(already sanitized)\" must trace back to explicit guard; RESUME/fallback modes need own guards", + "line": 393 + }, + { + "id": "RULESET.AUDIT.search-source-not-generated", + "klass": "RULESET", + "value": "verify an invariant/validation EXISTS by searching the AUTHORED source (src/*.cts OR the scripts/gen-*.cjs generator), never the generated bin/lib/*.cjs (gitignored, ADR-457); gen-time checks live in gen-*.cjs not the .cts it consumes → search BOTH before declaring absent; read generated .cjs only for output drift. Repro: grep src/*.cts for VALID_CONVERTER_NAMES → false \"5e ConverterName unenforced\"; actually enforced in gen-capability-registry.cjs. cf RULESET.TESTS.no-source-grep", + "line": 382 + }, + { + "id": "RULESET.CAPABILITY.cutover-self-gating", + "klass": "RULESET", + "value": "a phase-6 per-feature cutover moves the host's phase-context detection + mode/flag logic INTO the skill (self-gating, per ADR-894); the loop hook is intentionally COARSE — \"invoke skill X at point Y when config Z\" — and carries no detection/mode. WORKED EXAMPLE: plan-phase.md §5.6 UI gate (frontend-detection via ui-safety-gate.cjs + --auto/manual branch + --skip-ui bypass) must move into gsd-ui-phase before its plan:pre hook can replace the inline call without behavior loss. Spike #1018 finding.", + "line": 231 + }, + { + "id": "RULESET.CAPABILITY.off-means-off", + "klass": "RULESET", + "value": "the host derives shared outputs from the ACTIVE hook set (via loop.render-hooks); a hook may ADD a labeled block or be COUNTED into a host-computed aggregate (e.g. a score denominator), but NEVER mutates host source — so a disabled capability yields the base output by construction, not by authoring discipline. Ratify in ADR-894; proven by spike #1018.", + "line": 229 + }, + { + "id": "RULESET.CAPABILITY.precedence-engine-single-owner", + "klass": "RULESET", + "value": "the config-key four-level precedence walk (loadConfig result → workstream config.json → root config.json → registry.configSchema default → absent) is owned solely by src/capability-activation.cts: raw-value primitive resolveConfigKey(dotKey, {config,cwd,registry}) and boolean wrapper _resolveActivationValue(dotKey,config,cwd,registry); loop-resolver.cts imports the engine (no duplicate); resolveConfigValues in loop-resolver.cts delegates to resolveConfigKey; resolveCapabilityRuntimeState does NOT return registry/config — callers import capability-registry.cjs and call loadConfig(cwd) directly.", + "line": 235 + }, + { + "id": "RULESET.CAPABILITY.step-additive-gate-blocks", + "klass": "RULESET", + "value": "a `step` hook is purely additive (invoke skill + produce artifacts, NEVER halts the host); host-blocking preconditions are `gate`s (blocking:true, onError:halt); runtime/mode context (auto/chain vs manual) self-gates IN THE SKILL, not via `when` (config-only). §5.6 = plan:pre step (ui-phase; skill self-gates on frontend+pipeline, auto-fires only in pipelines) + a NEW plan:pre gate (frontend-and-no-UI-SPEC → halt, when:workflow.ui_safety_gate); the loop.render-hooks dispatch template handles steps AND gates. Resolves #1022.", + "line": 233 + }, + { + "id": "RULESET.CODERABBIT.GUARD.COMPLETE", + "klass": "RULESET", + "value": "required_checks_green && coderabbit_check_pass && graphQL(reviewThreads.unresolved_count)==0", + "line": 421 + }, + { + "id": "RULESET.CODERABBIT.GUARD.GRAPHQL", + "klass": "RULESET", + "value": "reviewThreads(first:100){nodes{id isResolved comments{nodes{author body path line originalLine url}}}}; use unresolved threads as authoritative, not badge text alone", + "line": 422 + }, + { + "id": "RULESET.CODERABBIT.GUARD.OPEN_PRS", + "klass": "RULESET", + "value": "gh pr list --repo open-gsd/gsd-core --author @me --state open; repeat near end because open PR set can change mid-run", + "line": 420 + }, + { + "id": "RULESET.CODERABBIT.GUARD.RERUN", + "klass": "RULESET", + "value": "after every push wait for CodeRabbit completion, then re-query unresolved threads; CodeRabbit can add new findings after earlier threads were resolved", + "line": 423 + }, + { + "id": "RULESET.CODERABBIT.GUARD.RESOLVE", + "klass": "RULESET", + "value": "fix validated finding -> focused tests -> commit/push -> resolveReviewThread(threadId) -> wait CI/CodeRabbit -> final unresolved_count query", + "line": 424 + }, + { + "id": "RULESET.CODERABBIT.GUARD.SCOPE", + "klass": "RULESET", + "value": "if a new @me open PR appears during final list, include it in the same guard pass before declaring all-open-PRs complete", + "line": 425 + }, + { + "id": "RULESET.CONTENT-PATH-NORMALIZATION", + "klass": "RULESET", + "value": "filesystem paths substituted into markdown body text (@-references, workflow .md, agent .md, generated docs, command bodies) MUST be normalized to POSIX forward slashes via .replace(/\\\\/g,'/') at the production source BEFORE substitution; never push normalization to tests; cross-platform content is POSIX-only; applies to: computePathPrefix output, install-path rewrites, generated shim paths emitted into .md bodies; idempotent on POSIX so unconditional", + "line": 743 + }, + { + "id": "RULESET.CONTRIB.CLASSIFY.enhancement", + "klass": "RULESET", + "value": "requires approved-enhancement before implementation", + "line": 414 + }, + { + "id": "RULESET.CONTRIB.CLASSIFY.feature", + "klass": "RULESET", + "value": "requires approved-feature before implementation", + "line": 415 + }, + { + "id": "RULESET.CONTRIB.CLASSIFY.fix", + "klass": "RULESET", + "value": "requires confirmed/confirmed-bug before implementation", + "line": 413 + }, + { + "id": "RULESET.CONTRIB.GATE.ORDER", + "klass": "RULESET", + "value": "issue-first -> approval-label -> code -> PR-link -> changeset/no-changelog", + "line": 412 + }, + { + "id": "RULESET.CR-THREAD-RESOLVE", + "klass": "RULESET", + "value": "after adding // allow-test-rule: to silence lint, resolve existing inline CR threads via graphql resolveReviewThread mutation before merge — open threads mislead future reviewers; pattern: gh api graphql -f query='mutation { resolveReviewThread(input:{threadId:\"PRRT_...\"}) { thread { isResolved } } }'", + "line": 406 + }, + { + "id": "RULESET.GEMINI.TEST_SENTINEL", + "klass": "RULESET", + "value": "convertClaudeToGeminiAgent regression should assert tools excludes ask_user, body excludes AskUserQuestion/ask_user, and Read still maps to read_file", + "line": 397 + }, + { + "id": "RULESET.GEMINI.TEST_SENTINEL", + "klass": "RULESET", + "value": "convertClaudeToGeminiAgent regression should assert tools excludes ask_user, body excludes AskUserQuestion/ask_user, and Read still maps to read_file", + "line": 429 + }, + { + "id": "RULESET.GEMINI.TOOLS.ask_user", + "klass": "RULESET", + "value": "Gemini CLI has no ask_user tool; filter both AskUserQuestion and lowercase ask_user from tools frontmatter and neutralize both names in body text", + "line": 396 + }, + { + "id": "RULESET.GEMINI.TOOLS.ask_user", + "klass": "RULESET", + "value": "Gemini CLI has no ask_user tool; filter both AskUserQuestion and lowercase ask_user from tools frontmatter and neutralize both names in Gemini body text", + "line": 428 + }, + { + "id": "RULESET.GH.AUTH.DEFAULT", + "klass": "RULESET", + "value": "source .envrc GITHUB_TOKEN before gh; exception=ambient allowed only when user explicitly says machine-only fallback", + "line": 419 + }, + { + "id": "RULESET.HARNESS.test-memory-guard", + "klass": "RULESET", + "value": "~/.claude/hooks/test-memory-guard.sh fires on every Bash PreToolUse; if argv[0]∈{node|vitest|jest|mocha|tsx|ts-node|tap|ava|playwright|cypress} OR matches (npm|pnpm|yarn|bun) (run )?(t|test|tests|vitest|jest); blocks via hookSpecificOutput.permissionDecision=deny when sum(RSS of running matching procs, excluding tsserver|*-mcp|claude|Electron|...) ≥ 4 GiB OR when argv[0] basename matches a running process's argv[0]. Exception: node --version|-v|--help|-h|-p|-e are trivial probes and skip the check. Designed for a 24 GB Mac where prior accidental fan-out exhausted RAM", + "line": 827 + }, + { + "id": "RULESET.MANIFEST-CANONICAL-KEY", + "klass": "RULESET", + "value": "docs/INVENTORY-MANIFEST.json has a single top-level key: families; ALL SIX families.* arrays (agents/commands/workflows/references/cli_modules/hooks) are canonical, consumed by test suites — tests/inventory-manifest-sync.test.cjs reads all six, edit-phase/enh-2380/enh-2430 tests read commands+workflows; the old generated date field and the stale top-level workflows key are both gone; regen via node scripts/gen-inventory-manifest.cjs --write", + "line": 400 + }, + { + "id": "RULESET.PR-FLOW.docker-before-push", + "klass": "RULESET", + "value": "before ANY git push of any fix to any PR, run gsd-test-summary (docker on the remote, mirrors ubuntu CI) and confirm exit 0. macOS-local node --test is NOT a substitute — many failures are platform-specific (path separators, case sensitivity, locale, fs semantics). Watchdog with Monitor on the output log; never set a sleep/timer and walk away. Source: user feedback 2026-05-16 — \"we don't set a timer we actively watch and record results in real time as possible\"", + "line": 829 + }, + { + "id": "RULESET.PR-FLOW.templates-mandatory", + "klass": "RULESET", + "value": "every gh pr create|edit|gh issue create|edit MUST first invoke the gh-templates-first skill and Read (Read tool, not Bash cat — k321 read-tracking) the matching template in .github/. Apply ALL required sections; never write freeform bodies. Repo enforces this via gsd-pr-template-policy GitHub Action which flags any non-templated body — the bot allows the PR to stay open only because authors are contributors-or-higher, but the warning is a real complaint that must be cured. Source: user feedback 2026-05-16 (multi-message escalation) — \"the whole reason i have that github action is because you fucking blow through and ignore using the templates\"", + "line": 831 + }, + { + "id": "RULESET.PR-SCOPE.one-concern-per-pr", + "klass": "RULESET", + "value": "split unrelated changes into separate PRs; cherry-pick doc changes to dedicated docs/ branch immediately, then force-push original to remove the commit", + "line": 402 + }, + { + "id": "RULESET.SHARED-HELPERS-LINT-VS-TEST", + "klass": "RULESET", + "value": "when a lint script and test suite both implement same constant (CANONICAL_TOOLS) or parser (parseFrontmatter, executionContextRefs), extract to scripts/*-helpers.cjs required by both — silent divergence otherwise", + "line": 394 + }, + { + "id": "RULESET.TESTS.CODERABBIT_FIX", + "klass": "RULESET", + "value": "prefer exported-function behavioral tests over source-grep; lint-no-source-grep rejects readFileSync source assertions without allow-test-rule", + "line": 426 + }, + { + "id": "RULESET.TESTS.boundary-coverage", + "klass": "RULESET", + "value": "tests MUST exercise inputs at and near the threshold/limit, not only trivial-fit and trivial-overflow; pick inputs where N ∈ {limit-1, limit, limit+1} and where pre-trim/pre-check accumulators ≈ effective limit; \"very small\" and \"very large\" inputs alone do not constitute edge-case coverage and routinely miss off-by-one + reservation-accounting bugs", + "line": 370 + }, + { + "id": "RULESET.TESTS.boundary-coverage.anti-pattern", + "klass": "RULESET", + "value": "test suites that pair budget:1_000_000 (trivially fits) with budget:1 (trivially overflows) and skip the boundary region; failure mode that shipped PR #3708 UNNEEDED_TRIM + FALSE_HARDFAIL regressions (commit 2df566ed, fixed bde1ae8f)", + "line": 372 + }, + { + "id": "RULESET.TESTS.boundary-coverage.fixtures", + "klass": "RULESET", + "value": "for any code with budget/limit/quota/threshold parameter, test suite MUST include: (a) input where SUT estimate == limit exactly, (b) input where estimate == limit - 1, (c) input where estimate == limit + 1, (d) input where any internal reserve/safety constant pushes baseline within reserve-distance of limit (catches early-pressure firing)", + "line": 371 + }, + { + "id": "RULESET.TESTS.clock-seam", + "klass": "RULESET", + "value": "concurrency logic must accept an optional {clock=Date} parameter; tests control time via t.mock.timers.enable(['Date']) + t.mock.timers.setTime(0) + t.mock.timers.tick(N); real OS scheduler races are not a permitted test pattern after ADR 456 (2026-05-28); real-race tests are deleted once deterministic seam tests cover the same logical path; clock.cjs realClock adds nowIso() (→ new Date(this.now()).toISOString()) and today() (→ nowIso().split('T')[0]) so all date-stamping in state.cjs routes through the seam; subprocess time-pin adapter: set GSD_TEST_MODE=1 + GSD_NOW_MS= in runGsdTools env to pin the date written by the SUT without touching real wall-clock (issue #474)", + "line": 376 + }, + { + "id": "RULESET.TESTS.coderabbit-fix-prefer", + "klass": "RULESET", + "value": "behavioral tests (call exported fn, capture JSON, assert typed fields) over source-grep", + "line": 368 + }, + { + "id": "RULESET.TESTS.delete-bad-tests", + "klass": "RULESET", + "value": "pass-always / vacuous-truth / source-grep / elapsed-time / real-race / permanent-allow-test-rule tests are DELETED and replaced with compliant tests in the same PR; not skipped, not commented out, not permanently exempted; replacement must cover the same logical path via typed-surface assertion or clock-seam pattern", + "line": 379 + }, + { + "id": "RULESET.TESTS.diagnostics", + "klass": "RULESET", + "value": "after JSON.parse, assert output shape (Array.isArray(output.phases)) with raw-output-prefix diagnostics before .map() — prevents opaque TypeErrors when CLI output shape changes", + "line": 369 + }, + { + "id": "RULESET.TESTS.escape-regex", + "klass": "RULESET", + "value": "new RegExp(\"prefix${var}\") must escapeRegex(var); phase-id.cjs exports escapeRegex (core.cjs re-export spine retired in epic #1267); phase IDs like 5.1 contain . which is metacharacter", + "line": 365 + }, + { + "id": "RULESET.TESTS.eslint-harness", + "klass": "RULESET", + "value": "ADR 452 (2026-05-28): ESLint flat config + typescript-eslint + eslint-plugin-n + eslint-plugin-no-only-tests + local plugin at scripts/eslint-rules/; replaces scripts/lint-*.cjs regex scanners; three test-rigor rules (local/no-source-grep, local/no-magic-sleep-in-tests, local/no-elapsed-assertion) ship at warn, promoted to error after #453 cleanup sweep merges", + "line": 380 + }, + { + "id": "RULESET.TESTS.guard-toplevel-readFileSync", + "klass": "RULESET", + "value": "module-level const src = readFileSync(...) throws before any test() registers — wrap in try/catch in test() or use lazy load", + "line": 367 + }, + { + "id": "RULESET.TESTS.mutation-score", + "klass": "RULESET", + "value": "Stryker runs incremental (--since origin/next) on ubuntu-latest/Node24 CI leg; default threshold 80% killed/total; surviving mutants in scope block merge unless path is listed in stryker.config.mjs with documented reason; treat surviving mutant as a failing test specification", + "line": 378 + }, + { + "id": "RULESET.TESTS.no-dead-regex-in-includes", + "klass": "RULESET", + "value": "src.includes(\"foo.*bar\") is always false — .* is regex metacharacter not wildcard; use new RegExp(...).test(src) or delete", + "line": 366 + }, + { + "id": "RULESET.TESTS.no-source-grep", + "klass": "RULESET", + "value": "scripts/lint-no-source-grep.cjs rejects readFileSync source + .includes()/.match()/.startsWith() on the bound var; CI hard-fail", + "line": 360 + }, + { + "id": "RULESET.TESTS.no-source-grep.exemption", + "klass": "RULESET", + "value": "// allow-test-rule: with one-line justification; reserved for tests where the file content IS the product surface (STATE.md, config.toml, hooks.json, agent .md). Migration to typed-IR parser tracked in #2974.", + "line": 362 + }, + { + "id": "RULESET.TESTS.no-source-grep.stdout-extension", + "klass": "RULESET", + "value": "also flags assert.match/doesNotMatch on .stdout/.stderr — emit JSON from SUT, parse, assert on typed fields", + "line": 361 + }, + { + "id": "RULESET.TESTS.no-source-grep.tmp-file-traps", + "klass": "RULESET", + "value": "reading tmp files written by the SUT in tests still trips lint; round-trip through CLI (e.g. frontmatter get) instead of readFileSync+.includes()", + "line": 363 + }, + { + "id": "RULESET.TESTS.no-timing-assertion", + "klass": "RULESET", + "value": "do not assert on wall-clock elapsed time (Date.now() delta, performance.now(), process.hrtime() comparison); such assertions test the host machine not the SUT and flake on loaded CI runners; enforcement: local/no-elapsed-assertion ESLint rule (warn → error after #453); canonical replacement: clock-seam pattern with node:test mock.timers", + "line": 375 + }, + { + "id": "RULESET.TESTS.property-based-testing", + "klass": "RULESET", + "value": "modules implementing parsing / transformation / budget-limit / bijective contracts must include at least one fast-check (fc) property test asserting a domain invariant; invariant categories: round-trip, monotonicity, boundary-containment, idempotency; property tests live in *.test.cjs alongside unit tests; CI signal: Stryker mutation score below 80% blocks merge", + "line": 377 + }, + { + "id": "RULESET.TRIAGE-EXISTING-WORK", + "klass": "RULESET", + "value": "before writing agent brief for confirmed bug, check (1) local branches git branch -a | grep , (2) untracked/modified files on that branch, (3) stash, (4) open PRs with matching head branch — recover existing work rather than re-implement", + "line": 404 + }, + { + "id": "RULESET.WORKFLOW.COVERAGE-METADATA", + "klass": "RULESET", + "value": "#1602 SUMMARY frontmatter `coverage:` block (list of {id,description,requirement?,verification:[{kind∈unit|integration|e2e|automated_ui|manual_procedural|other, ref, status∈pass|fail|unknown}],human_judgment:bool,rationale?}) is the per-deliverable RTM consumed DETERMINISTICALLY by verify-work extract_tests via `gsd-tools uat classify-coverage --summary ` (src/coverage.cts → bin/lib/coverage.cjs). AUTHORING: execute-plan create_summary populates it from task results; every deliverable MUST be classified; fail-safe default = human_judgment:true + rationale. CLASSIFY CONTRACT: auto-pass (skip human) ONLY when human_judgment===false (strict boolean) AND verification non-empty AND every status==='pass' AND zero validation errors — else PRESENT to human. mode:legacy (no block) ⇒ byte-identical prose `## Accomplishments` fall-through; `coverage: []` ⇒ mode:coverage, zero entries (single-confirmation). Frozen IR: MODE/PRESENT_REASON/ERROR_CODE enums locked by tests/coverage-metadata-parser.test.cjs. extractFrontmatter CANNOT parse it (scalars-only `-` items) → dedicated parser, sibling of parseMustHavesBlock. Asymmetry by design: false-negative=redundant prompt (status quo); false-positive=shipped bug UAT existed to catch", + "line": 390 + }, + { + "id": "RULESET.WORKFLOW_EXECUTE_END_TO_END", + "klass": "RULESET", + "value": "ADR-0002 standard for single-workflow commands is \"Execute end-to-end.\" (no bolded **Follow the X workflow** fragments); flag-dispatch routing uses \"execute the X workflow end-to-end.\" in routing bullets", + "line": 389 + }, + { + "id": "RULESET.WORKFLOW_EXECUTION_CONTEXT", + "klass": "RULESET", + "value": "@-ref in commands/gsd/*.md must resolve to an existing file on disk; regression test in tests/bug-3135-capture-backlog-workflow.test.cjs; INVENTORY.md row + INVENTORY-MANIFEST.json families.workflows must stay in sync; \"Invoked by\" attribution must move when a flag absorbs a micro-skill", + "line": 388 + }, + { + "id": "RULESET.WORKFLOW_FILE_NAMES", + "klass": "RULESET", + "value": "workflow files use hyphens; XML attributes must match (extract-learnings not extract_learnings); tests should pin exact hyphenated name", + "line": 387 + }, + { + "id": "RULESET.WORKFLOW_MARKDOWN.FENCES", + "klass": "RULESET", + "value": "preserve opening language fence when editing shell snippets in workflow markdown; malformed fence creates fresh CR threads (MD040)", + "line": 384 + }, + { + "id": "RULESET.WORKFLOW_MARKDOWN.FENCES", + "klass": "RULESET", + "value": "when editing shell snippets inside workflow markdown, preserve the opening language fence; malformed fence can create fresh CodeRabbit threads", + "line": 427 + }, + { + "id": "RULESET.WORKFLOW_SIZE_BUDGET", + "klass": "RULESET", + "value": "workflow size enforcement (#1074; BYTES not lines per #717; LF-normalized per #683) = per-file baseline (PRIMARY anti-creep: tests/workflow-size-baseline.json pins each file's exact size) + loose tier hard caps (outer red lines, NEVER raised on approach: XL<=98304 / LARGE<=61440 / DEFAULT<=40960) + new-file cap (un-baselined files <32768, the Codex anchor) + discuss-phase<32000; a file that grew fails the baseline guard — fix with `npm run size:baseline`, commit the one-line diff, and justify the growth in the PR (or extract LAZILY-loaded content; eager @-imports don't reduce loaded context); crossing a hard cap means EXTRACT, not bump", + "line": 385 + }, + { + "id": "SESSION.2026-05-05", + "klass": "SESSION", + "value": "[PRED.k320..k331 introduced; DEFECT.SOURCE-GREP-IN-NEW-TESTS, DEFECT.CHANGESET-PR-FIELD-DRIFT, DEFECT.PHASE-DIR-PREFIX-DRIFT, DEFECT.PROMPT-INJECTION-SCAN-COLLISION; ADR-0002 thin-wrapper pattern findings folded into RULESET.WORKFLOW_*]", + "line": 783 + }, + { + "id": "SESSION.2026-05-05.sdk-bridge", + "klass": "SESSION", + "value": "PR #3158 SDK Runtime Bridge — observability isolation rule; strict-mode dispatchMode reporting invariant; transport decision ordering (guard before event emission); folded into Dispatch Policy Module glossary", + "line": 784 + }, + { + "id": "SESSION.2026-05-09", + "klass": "SESSION", + "value": "[8-PR triage wave, 7 merged + 1 subsumed; META.RULE.* introduced; WAVE.LESSON.* captured; k320/k322/k323/k326/k331 evidence; AI Ops Memory predicate format established]", + "line": 785 + }, + { + "id": "SESSION.2026-05-10", + "klass": "SESSION", + "value": "[ai-ops memory consolidation; release-notes standard taxonomy + templates; RELEASE-NOTES.* predicates introduced]", + "line": 786 + }, + { + "id": "SESSION.2026-05-13", + "klass": "SESSION", + "value": "[Shell Command Projection Module expansion (#3465-#3468); ADR-0009 superseded; new exports for subprocess dispatch and platform file I/O; phase-gated migration plan; PR #3464 three-gate invariant CI+CR+unresolved=0; PR #3470 stash-include-untracked rebase pattern]", + "line": 787 + }, + { + "id": "SESSION.2026-05-14", + "klass": "SESSION", + "value": "[#3095/PR #3490 EXEC.CLASSIFY.* introduced (Anthropic/Copilot/Codex/Gemini cross-runtime rate-limit sentinel coverage); #3489/PR #3499 DEFECT.STATE-TRAMPLE.idempotency-oracle (STATE.md current_phase field is oracle for state.complete-phase); #3488/PR #3501 DAG resolver same-phase short-form depends_on (shortFormToId index added to sdk/src/query/phase.ts); #3491/PR #3502 DEFECT.NESTED-GIT-INIT (gitWorktreeInfoInternal helper); #3493/PR #3500 extractCurrentMilestone generic Phase Details continuation past planned-milestone siblings; #3503/PR #3504 DEFECT.PATH-SUBSTRING-CHECK (trailing-slash anchor for homedir checks); #3346/PR #3505 codex AoT TOML leaf-key via extractFlatHookEventName; #3506/PR #3507 label-scoped stale-bot sub-job pattern; multi-PR triage operational lessons folded into PROC.TRIAGE.*; #3508 DEFECT.AGENT-ISOLATION-SILENT-FAIL; gsd-test image-missing auto-build (locally-built image via embedded heredoc Dockerfile); refined PRED.k322 threshold to 3 PRs/<10min]", + "line": 788 + }, + { + "id": "SESSION.2026-05-15", + "klass": "SESSION", + "value": "[#3537/PR #3538 DEFECT.PHASE-REGEX-FANOUT — phaseMarkdownRegexSource promoted to core.cjs and wired to 7 sites; parity-style regression test established as DEFECT.GENERATIVE-FIX exemplar; trek-e/gsd-test-runner#1 filed for DEFECT.GSD-TEST-MIRROR-POISONED — chown-back-before-exec legacy gap (poisoned holodeck mirror unstuck via authorized docker chown to remote 1000:1000); RULESET.PR-FLOW.* codified from project CLAUDE.md load-bearing rule; first dispatch under run-tests-before-create held cleanly (PR #3520 worker stopped on Docker exit 12 infra failure, orchestrator opened PR after unblock); CONTEXT.md refactored from 882 lines of mixed prose+predicates into ~500 lines of pure-predicate format with chronological session log]", + "line": 789 + }, + { + "id": "SESSION.2026-05-15.parallel-fix-dispatch", + "klass": "SESSION", + "value": "[#3542/PR #3546 prohibit git stash family in executor agents (shared refs/stash across worktrees); #3541/PR #3547 non-TTY resolution for installer prompt-user actions (default remove for SDK build artifacts, keep for skills/gsd-*/SKILL.md); #3545 filed for gsd-test-summary concurrent /tmp output collision; new predicates DEFECT.HOOK-OVER-ENFORCEMENT.read-tool-tracking, DEFECT.GSD-TEST-CONCURRENT-OUTPUT-COLLISION, DEFECT.SUBAGENT-LONG-RUNNING-BG-STALL, DEFECT.AGENT-RETIRED-SLASH-SYNTAX-DRIFT, PROC.PARALLEL-FIX-DISPATCH; agent-trust-but-verify caught /gsd-update retired-syntax comment slip in #3541 implementation before PR open]", + "line": 790 + }, + { + "id": "SESSION.2026-05-16", + "klass": "SESSION", + "value": "[multi-PR triage wave (#3577/3581/3640/3641/3642/3648/3649/3637/3639). Established global PreToolUse hook ~/.claude/hooks/test-memory-guard.sh denying new node/test spawns when sum(RSS of node|vitest|jest|...) >= 4 GiB on the 24 GB Mac OR when a same-runner process is already in argv[0] — hard deny via hookSpecificOutput.permissionDecision=deny. PR #3577 fix: revert config-ensure-section dispatch to CJS cmdConfigEnsureSection (SDK author wrote single-section semantics under a name whose legacy callers expect full-default config init); plus 3 SDK parity carve-outs (configNewProject defaults align with sdk/shared/config-defaults.manifest.json, return relative .planning/config.json path, drop quotes from Unknown config key, lead malformed-JSON error with \"Failed to read config.json:\"). PR #3649 fix: chunk node --test spawn at 28K argv ceiling (Windows CreateProcess lpCommandLine cap 32,767 was instantly aborting unchunked spawn of 546 paths). Chunking fix surfaced 14 pre-existing Windows-only test bugs (4010 pass / 14 fail; vs 0/0 before — entire suite was un-runnable on Windows). PRs #3639 + #3637 confirmed unable to stand alone (legitimately depend on Phase 6 scaffolding only present on feat/3575-enforcement-hardening) — user decision: cherry-pick into #3577 and close. Five other PRs each had ≤1 unresolved CR thread of the changeset-pr-number / null-vs-throw / implicit-Claude-runtime / docs-stale-guidance / hardcoded-tests-path family — all quick wins. New predicates: DEFECT.SDK-PORT-NAME-COLLISION, DEFECT.WINDOWS-ARGV-OVERFLOW, DEFECT.STACKED-PR-CANNOT-STAND-ALONE, DEFECT.CANARY-VERSION-LEAK, DEFECT.GSD-TEST-HOST-MID-RUN-DEATH, RULESET.HARNESS.test-memory-guard, RULESET.PR-FLOW.docker-before-push, RULESET.PR-FLOW.templates-mandatory]", + "line": 791 + }, + { + "id": "WAVE.LESSON.agent-narrative-unreliable", + "klass": "WAVE", + "value": "k095/k324 confirmed at scale: 5 of 8 agents terminated mid-monitor with stale claims requiring direct verification", + "line": 612 + }, + { + "id": "WAVE.LESSON.changelog-policy-violation-multiplier", + "klass": "WAVE", + "value": "brief contradicting CONTRIBUTING.md L110 produced violations on 5 of 8 PRs (#3300, #3302, #3304, #3305, #3308); k326 + k320 capture", + "line": 609 + }, + { + "id": "WAVE.LESSON.cr-throttle-burst-correlation", + "klass": "WAVE", + "value": "8 PRs in <15min triggered k322 sustained-throttle on multiple PRs (#3306 worst case)", + "line": 610 + }, + { + "id": "WAVE.LESSON.k101-still-trips", + "klass": "WAVE", + "value": "even after CONTEXT.md k101 reinforcement, agent of record posted self-PR comment on close; k331 adds explicit close-time literal-instruction guard", + "line": 613 + }, + { + "id": "WAVE.LESSON.sibling-audit-overlap", + "klass": "WAVE", + "value": "k015-family parallel dispatch on #3297 + #3298 produced k323 add-backlog.md cross-PR overlap", + "line": 611 + }, + { + "id": "WORKSTREAM.INVARIANT.migrate-name", + "klass": "WORKSTREAM", + "value": "must normalize through canonical slug policy", + "line": 444 + }, + { + "id": "WORKSTREAM.INVARIANT.slug-contract", + "klass": "WORKSTREAM", + "value": "all .planning/workstreams/ must be addressable by set/get/status/complete", + "line": 445 + }, + { + "id": "WORKSTREAM.NAME.POLICY.cjs-module", + "klass": "WORKSTREAM", + "value": "gsd-core/bin/lib/workstream-name-policy.cjs owns toWorkstreamSlug + active-name/path-segment validation", + "line": 461 + }, + { + "id": "WORKSTREAM.POINTER.SEAM.cjs-module", + "klass": "WORKSTREAM", + "value": "gsd-core/bin/lib/active-workstream-store.cjs owns read/write self-heal for .planning/active-workstream", + "line": 462 + }, + { + "id": "WORKSTREAM.REGRESSION.test-anchor", + "klass": "WORKSTREAM", + "value": "tests/workstream.test.cjs::normalizes --migrate-name to a valid workstream slug", + "line": 446 + }, + { + "id": "WORKTREE.SEAM.caller-rule", + "klass": "WORKTREE", + "value": "verify.cjs must consume inspectWorktreeHealth for W017 classification; no ad-hoc porcelain parsing in callers", + "line": 455 + }, + { + "id": "WORKTREE.SEAM.current", + "klass": "WORKTREE", + "value": "Worktree Safety Policy Module", + "line": 438 + }, + { + "id": "WORKTREE.SEAM.decision-1", + "klass": "WORKTREE", + "value": "retain non-destructive default; destructive path only as explicit future opt-in scaffold", + "line": 442 + }, + { + "id": "WORKTREE.SEAM.default-prune-policy", + "klass": "WORKTREE", + "value": "metadata_prune_only (non-destructive)", + "line": 441 + }, + { + "id": "WORKTREE.SEAM.execution-rule", + "klass": "WORKTREE", + "value": "prefer node --test tests/worktree-safety-policy.test.cjs for fast seam validation; avoid full npm test loop for seam-only changes", + "line": 453 + }, + { + "id": "WORKTREE.SEAM.files", + "klass": "WORKTREE", + "value": "[gsd-core/bin/lib/worktree-safety.cjs]", + "line": 439 + }, + { + "id": "WORKTREE.SEAM.interface", + "klass": "WORKTREE", + "value": "[resolveWorktreeContext, parseWorktreePorcelain, planWorktreePrune, executeWorktreePrunePlan, planWorktreeRecordAgent, cmdWorktreeRecordAgent]", + "line": 440 + }, + { + "id": "WORKTREE.SEAM.invariant", + "klass": "WORKTREE", + "value": "parser failure must degrade to metadata_prune_only and never escalate to destructive removal", + "line": 452 + }, + { + "id": "WORKTREE.SEAM.inventory-interface", + "klass": "WORKTREE", + "value": "[listLinkedWorktreePaths, inspectWorktreeHealth]", + "line": 454 + }, + { + "id": "WORKTREE.SEAM.inventory-snapshot", + "klass": "WORKTREE", + "value": "snapshotWorktreeInventory(repoRoot,{staleAfterMs,nowMs}) is canonical linked-worktree health snapshot for callers", + "line": 457 + }, + { + "id": "WORKTREE.SEAM.test-anchor-w017", + "klass": "WORKTREE", + "value": "tests/orphan-worktree-detection.test.cjs + tests/worktree-safety-policy.test.cjs", + "line": 456 + }, + { + "id": "WORKTREE.SEAM.test-anchors", + "klass": "WORKTREE", + "value": "[resolveWorktreeContext:has_local_planning|linked_worktree|not_git_repo|main_worktree, planWorktreePrune:git_list_failed|worktrees_present|no_worktrees|parser_throw_fallback, executeWorktreePrunePlan:missing_plan|skip_passthrough|unsupported_action|metadata_prune_only]", + "line": 451 + }, + { + "id": "WORKTREE.SEAM.test-policy", + "klass": "WORKTREE", + "value": "cover all decision branches in policy module before changing prune behavior", + "line": 450 + } + ], + "duplicates": [ + { + "id": "RULESET.GEMINI.TEST_SENTINEL", + "lines": [ + 397, + 429 + ] + }, + { + "id": "RULESET.GEMINI.TOOLS.ask_user", + "lines": [ + 396, + 428 + ] + }, + { + "id": "RULESET.WORKFLOW_MARKDOWN.FENCES", + "lines": [ + 384, + 427 + ] + } + ] +} diff --git a/examples/dynamic-context-management/README.md b/examples/dynamic-context-management/README.md new file mode 100644 index 000000000..9f759fba7 --- /dev/null +++ b/examples/dynamic-context-management/README.md @@ -0,0 +1,44 @@ +# Dynamic context management — Option-E reference example + +Reference example for [ADR-1671](../../docs/adr/1671-dynamic-context-management-platform.md), +"Dynamic context management platform." + +> **This is a non-shipping reference example.** It lives outside the build +> (`src/` → `bin/lib/`), the npm package `files[]`, the installer, and the CI +> test suite (`tests/`). Nothing here is compiled into or installed with GSD. +> The production implementation lands in a later phase of the +> [Dynamic Context Management epic (#1671)](https://github.com/open-gsd/gsd-core/issues/1671). + +## What it demonstrates + +The **predicate fact-store → JIT selector** slice of the platform: parse the +repo-root `CONTEXT.md` `CLASS.subkey=value` predicates into structured records, +drift-guard a generated index, and select the relevant predicate subset for a +task — the building block for just-in-time agent-brief assembly instead of +hand-citing a 200 KB file. + +## Files + +- `context-predicates.cjs` — parser + selector + deterministic index builder (self-contained). +- `gen-context-index.cjs` — `--check` / `--write` drift-guarded generator + `--select`. +- `CONTEXT-INDEX.json` — sample generated output (393 predicates, 18 classes). +- `demo.cjs` — runnable usage example. + +## Run (from the repo root) + +```sh +node examples/dynamic-context-management/demo.cjs +node examples/dynamic-context-management/gen-context-index.cjs --select PRED.k320 +node examples/dynamic-context-management/gen-context-index.cjs --check +``` + +## Validation + +During research this slice was validated with 42 behavioral tests — predicate +forms, fenced-code / prose skipping, duplicate-id detection, the selector, a +deterministic index, and a fast-check property test. Those return as CI tests +under `tests/` when the production implementation lands. + +It also surfaced 3 latent duplicate predicate IDs in `CONTEXT.md` +(`RULESET.WORKFLOW_MARKDOWN.FENCES`, `RULESET.GEMINI.TOOLS.ask_user`, +`RULESET.GEMINI.TEST_SENTINEL`), recorded in the index `duplicates` field. diff --git a/examples/dynamic-context-management/context-predicates.cjs b/examples/dynamic-context-management/context-predicates.cjs new file mode 100644 index 000000000..a3464f78d --- /dev/null +++ b/examples/dynamic-context-management/context-predicates.cjs @@ -0,0 +1,248 @@ +'use strict'; + +/** + * context-predicates.cjs — CONTEXT.md predicate fact-store parser. + * + * Self-contained CommonJS module (no dependency on build:lib output). + * + * Exports: + * parsePredicates(markdown) -> { predicates, duplicates, skippedSections } + * selectPredicates(predicates, { klass, prefix, contains }) -> filtered array + * buildIndex(predicates) -> deterministic plain object + * + * Grammar (from discovery facts): + * Two line forms, each on exactly one source line: + * 1. Bare backtick-wrapped: `ID=value` + * 2. List-item backtick: - `ID=value` + * + * ID grammar: CLASS(.subkey)* where CLASS = first dot-separated segment. + * ID chars: [A-Za-z0-9._-] (CLASS always uppercase; subkeys may be mixed). + * Split on FIRST '=' only; everything before is the ID, everything after is + * the value (up to the closing backtick). + * + * Skip: + * - Fenced code blocks (toggle on triple-backtick lines) + * - Prose lines (headings, blank lines, list items without a predicate) + * - The "PR fix discipline" section (pure prose, no predicates) + * - Session-log blockquote preamble + */ + +// Regex matching the predicate ID grammar: one or more dot-separated segments. +// First segment must start with an uppercase letter (CLASS). +// Subsequent segments may start with letter/digit and include hyphens/underscores. +// We intentionally allow lowercase-starting sub-segments (e.g. PRED.k320.rule). +const ID_RE = /^([A-Z][A-Z0-9_-]*(?:\.[A-Za-z0-9_.-]+)*)=(.+)$/; + +/** + * Parse a single source line and return a raw {id, value} if it is a predicate, + * or null otherwise. Handles both line forms after stripping list markers. + * + * @param {string} raw - the original source line (with newline stripped) + * @returns {{ id: string, value: string } | null} + */ +function extractPredicate(raw) { + const line = raw.trimEnd(); + + // Form 1: `ID=value` (starts with backtick at column 0) + // Form 2: - `ID=value` (list-item with leading "- ") + // Also tolerate " - `ID=value`" (indented list item — observed in CONTEXT.md). + let inner = null; + + if (line.startsWith('`') && line.endsWith('`') && line.length > 2) { + // bare backtick line + inner = line.slice(1, -1); + } else { + // strip optional leading whitespace + "- " then check for backtick wrapping + const stripped = line.replace(/^\s*-\s+/, ''); + if (stripped.startsWith('`') && stripped.endsWith('`') && stripped.length > 2) { + inner = stripped.slice(1, -1); + } + } + + if (inner === null) return null; + + // Now match the ID grammar. Split on FIRST '=' only. + const eqIdx = inner.indexOf('='); + if (eqIdx < 1) return null; + + const id = inner.slice(0, eqIdx); + const value = inner.slice(eqIdx + 1); + + // Validate ID — must match the grammar (no spaces, correct char set). + if (!ID_RE.test(inner)) return null; + + return { id, value }; +} + +/** + * Parse all predicates from a CONTEXT.md markdown string. + * + * @param {string} markdown + * @returns {{ + * predicates: Array<{ id: string, klass: string, value: string, line: number, section: string }>, + * duplicates: Array<{ id: string, lines: number[] }>, + * skippedSections: string[] + * }} + */ +function parsePredicates(markdown) { + const lines = markdown.split('\n'); + const predicates = []; + // Track id -> list of line numbers for duplicate detection + const idLines = new Map(); // id -> number[] + + let inFencedCode = false; + let currentSection = ''; + const allSections = []; + const seenSections = new Set(); + + // Section names that are known pure-prose (0 predicates) — we still scan them + // but track them as skipped if nothing is found. The parser is tolerant; it + // simply won't find predicates in prose sections. + // We do NOT hard-skip any section except fenced code — the grammar says "scan + // for backtick predicates everywhere but skip fenced code". + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + const lineNo = i + 1; // 1-based + + // Track fenced code blocks (triple-backtick toggle). + // A fenced-code fence starts with ``` possibly followed by a language token. + // We use a simple heuristic: a line trimmed to /^```/ triggers the toggle. + const trimmed = raw.trimStart(); + if (trimmed.startsWith('```')) { + inFencedCode = !inFencedCode; + continue; + } + + if (inFencedCode) continue; + + // Track section headings for the section field. + if (raw.startsWith('#')) { + currentSection = raw.replace(/^#+\s*/, '').trim(); + if (currentSection && !seenSections.has(currentSection)) { + seenSections.add(currentSection); + allSections.push(currentSection); + } + continue; + } + + // Blockquote lines (start with ">") are prose — skip. + if (trimmed.startsWith('>')) continue; + + // Attempt extraction. + const pred = extractPredicate(raw); + if (!pred) continue; + + const klass = pred.id.split('.')[0]; + predicates.push({ + id: pred.id, + klass, + value: pred.value, + line: lineNo, + section: currentSection, + }); + + const existing = idLines.get(pred.id); + if (existing) { + existing.push(lineNo); + } else { + idLines.set(pred.id, [lineNo]); + } + } + + // Build duplicates list: ids with >1 occurrence. + const duplicates = []; + for (const [id, lns] of idLines) { + if (lns.length > 1) { + duplicates.push({ id, lines: lns }); + } + } + // Sort duplicates by id for determinism. + duplicates.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0); + + // Skipped sections: headings that yielded zero predicates (pure prose). + const activeSections = new Set(predicates.map((p) => p.section)); + const skippedSections = allSections.filter((s) => !activeSections.has(s)); + + return { predicates, duplicates, skippedSections }; +} + +/** + * Select predicates by one or more optional criteria (ANDed together). + * + * @param {Array<{ id: string, klass: string, value: string, line: number, section: string }>} predicates + * @param {{ klass?: string, prefix?: string, contains?: string }} opts + * @returns {Array<{ id: string, klass: string, value: string, line: number, section: string }>} + */ +function selectPredicates(predicates, opts = {}) { + const { klass, prefix, contains } = opts; + const containsLower = contains ? contains.toLowerCase() : null; + + return predicates.filter((p) => { + if (klass !== undefined && p.klass !== klass) return false; + if (prefix !== undefined && !p.id.startsWith(prefix)) return false; + if (containsLower !== null) { + const haystack = (p.id + ' ' + p.value).toLowerCase(); + if (!haystack.includes(containsLower)) return false; + } + return true; + }); +} + +/** + * Build a deterministic index object from a parsed predicates array. + * + * @param {Array<{ id: string, klass: string, value: string, line: number }>} predicates + * @returns {{ + * schemaVersion: 1, + * count: number, + * classes: Record, + * predicates: Array<{ id: string, klass: string, value: string, line: number }>, + * duplicates: Array<{ id: string, lines: number[] }> + * }} + */ +function buildIndex(predicates) { + // Count per class. + const classCounts = {}; + for (const p of predicates) { + classCounts[p.klass] = (classCounts[p.klass] || 0) + 1; + } + + // Sort classes object by key for determinism. + const classes = {}; + for (const k of Object.keys(classCounts).sort()) { + classes[k] = classCounts[k]; + } + + // Sort predicates by id then by line number. + const sortedPredicates = predicates + .map(({ id, klass, value, line }) => ({ id, klass, value, line })) + .sort((a, b) => { + if (a.id < b.id) return -1; + if (a.id > b.id) return 1; + return a.line - b.line; + }); + + // Rebuild duplicates from sorted predicates for determinism. + const idToLines = new Map(); + for (const p of sortedPredicates) { + const arr = idToLines.get(p.id); + if (arr) arr.push(p.line); + else idToLines.set(p.id, [p.line]); + } + const duplicates = []; + for (const [id, lines] of idToLines) { + if (lines.length > 1) duplicates.push({ id, lines }); + } + duplicates.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0); + + return { + schemaVersion: 1, + count: predicates.length, + classes, + predicates: sortedPredicates, + duplicates, + }; +} + +module.exports = { parsePredicates, selectPredicates, buildIndex }; diff --git a/examples/dynamic-context-management/demo.cjs b/examples/dynamic-context-management/demo.cjs new file mode 100644 index 000000000..69a218da4 --- /dev/null +++ b/examples/dynamic-context-management/demo.cjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +'use strict'; + +/** + * Runnable usage example for the Option-E predicate fact-store (ADR-1671). + * Reference example only — not shipped, not part of the CI suite. + * + * node examples/dynamic-context-management/demo.cjs + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const { parsePredicates, selectPredicates } = require('./context-predicates.cjs'); + +const md = fs.readFileSync(path.resolve(__dirname, '..', '..', 'CONTEXT.md'), 'utf8'); +const { predicates, duplicates } = parsePredicates(md); +const classes = new Set(predicates.map((p) => p.klass)); + +process.stdout.write( + `Parsed ${predicates.length} predicates across ${classes.size} classes; ` + + `${duplicates.length} duplicate id(s).\n`, +); + +const slice = selectPredicates(predicates, { prefix: 'PRED.k320' }); +process.stdout.write( + `\nselectPredicates({ prefix: 'PRED.k320' }) -> ${slice.length} matches ` + + `(a JIT brief slice):\n`, +); +for (const p of slice) process.stdout.write(` ${p.id} = ${p.value}\n`); diff --git a/examples/dynamic-context-management/gen-context-index.cjs b/examples/dynamic-context-management/gen-context-index.cjs new file mode 100644 index 000000000..b771e0c6c --- /dev/null +++ b/examples/dynamic-context-management/gen-context-index.cjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +'use strict'; + +/** + * Reference example (NOT shipped, NOT compiled, NOT installed) for ADR-1671, + * "Dynamic context management platform" — the Option-E predicate fact-store. + * + * Builds a deterministic, drift-guarded index of every predicate fact in the + * repo-root CONTEXT.md, and demonstrates a JIT "task -> relevant predicates" + * selector. Self-contained: depends only on the sibling context-predicates.cjs. + * + * Usage (run from the repo root): + * node examples/dynamic-context-management/gen-context-index.cjs # print index to stdout + * node examples/dynamic-context-management/gen-context-index.cjs --write # write CONTEXT-INDEX.json (next to this file) + * node examples/dynamic-context-management/gen-context-index.cjs --check # exit 1 if the committed sample is stale + * node examples/dynamic-context-management/gen-context-index.cjs --select + * + * --select tries, in order: exact class ("PRED"), dotted prefix + * ("PRED.k320"), then free-text contains — the first non-empty match wins. + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const { parsePredicates, selectPredicates, buildIndex } = require('./context-predicates.cjs'); + +const REPO_ROOT = path.resolve(__dirname, '..', '..'); +const CONTEXT_PATH = path.join(REPO_ROOT, 'CONTEXT.md'); +const INDEX_PATH = path.join(__dirname, 'CONTEXT-INDEX.json'); + +function buildFreshIndex() { + const markdown = fs.readFileSync(CONTEXT_PATH, 'utf8'); + const { predicates } = parsePredicates(markdown); + return buildIndex(predicates); +} + +function main(args) { + const flag = args[0]; + + if (flag === '--check') { + const committed = JSON.parse(fs.readFileSync(INDEX_PATH, 'utf8')); + const live = buildFreshIndex(); + if (JSON.stringify(committed, null, 2) !== JSON.stringify(live, null, 2)) { + process.stderr.write( + 'CONTEXT-INDEX.json is stale. Run:\n' + + ' node examples/dynamic-context-management/gen-context-index.cjs --write\n', + ); + return 1; + } + process.stdout.write('CONTEXT-INDEX.json is up to date.\n'); + return 0; + } + + if (flag === '--write') { + const index = buildFreshIndex(); + fs.writeFileSync(INDEX_PATH, JSON.stringify(index, null, 2) + '\n'); + const dupNote = index.duplicates.length > 0 + ? ` (${index.duplicates.length} duplicate id${index.duplicates.length !== 1 ? 's' : ''})` + : ''; + process.stdout.write( + `Wrote ${path.relative(REPO_ROOT, INDEX_PATH)}\n` + + ` ${index.count} predicates, ${Object.keys(index.classes).length} classes${dupNote}\n`, + ); + return 0; + } + + if (flag === '--select') { + const query = args[1]; + if (!query) { + process.stderr.write('Usage: gen-context-index.cjs --select \n'); + return 1; + } + const { predicates } = parsePredicates(fs.readFileSync(CONTEXT_PATH, 'utf8')); + let results = selectPredicates(predicates, { klass: query }); + if (results.length === 0) results = selectPredicates(predicates, { prefix: query }); + if (results.length === 0) results = selectPredicates(predicates, { contains: query }); + if (results.length === 0) { + process.stdout.write(`No predicates matched: ${query}\n`); + return 0; + } + for (const p of results) process.stdout.write(`${p.id} = ${p.value}\n`); + process.stdout.write(`\n(${results.length} predicate${results.length !== 1 ? 's' : ''} matched)\n`); + return 0; + } + + process.stdout.write(JSON.stringify(buildFreshIndex(), null, 2) + '\n'); + return 0; +} + +process.exitCode = main(process.argv.slice(2));