* feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2]
Phase 3 slice 2 (AC2). The engine-as-library path: createImperativeAdapter
composes loadRegistry({includeInstalled:true}) — first-party-wins + consent +
fail-closed gates, identical trust semantics to the CLI — and binds the engine
surface behind the SAME HostIntegrationInterface the declarative adapter (AC1)
satisfies, plus a registry accessor for the composed capability set.
- src/adapter-imperative.cts: createImperativeAdapter({runtime}, {loadOptions})
→ ImperativeAdapter (kind:'imperative' + .registry + install/uninstall
delegating to install-engine). Thin: delegates the loop, does not reimplement.
- tests/adapter-imperative.test.cjs: kind (16 runtimes), registry composition
(loadRegistry called with includeInstalled:true), loadOptions pass-through,
install/uninstall delegation, fail-closed construction.
- eslint.config.mjs + docs/INVENTORY-MANIFEST.json: ADR-457 ignores entry +
cli_modules entry for the new emitted .cjs (the two drift gates that bit AC1,
applied proactively here).
Concrete host binding (OpenCode/VS Code/pi) deferred to Phase 5 (#1682).
* test: remove dead readStateMd helper from bug-1760 test
readStateMd was defined but never called (writeStateMd is the only state-md
helper this test uses). Clears the lone no-unused-vars warning so the repo
lints fully clean (0 problems). No behavior change — test still passes 2/2.
* chore(changeset): add Changed fragment for imperative embedding adapter (#1680)
* fix(adapter-imperative): reword comment to avoid injection-scan substring match
The prompt-injection scan regex 'act\s+as\s+(?:a|an|the)' was matching the
'act as the' substring inside 'contract as the declarative adapter' (contrACT
AS THE). Reword 'contract as' -> 'shape as' — no 'act' substring, identical
meaning. Clears the 'lib source files are clean' + 'codebase prompt injection
scan' security-gate failures.
* feat(#1680): ADR-1239 Phase C-1 — declarative embedding adapter + minimal HostIntegrationInterface [AC1]
Phase 3 slice 1 (AC1). Names + bounds today's projection path behind the common
HostIntegrationInterface that both declarative + imperative adapters will satisfy.
- src/embedding-adapter.cts: minimal HostIntegrationInterface (kind + runtime +
install/uninstall) + ADAPTER_KINDS. The full 6-point binding surface
(command/dispatch/model/hooks/state/artifact) is DEFERRED until the imperative
adapter (AC2) fixes the shape — ADR-1239 lists the wire-shape as an open
question; freezing it now risks rework across Phases 3-6.
- src/adapter-declarative.cts: createDeclarativeAdapter({runtime}) factory.
Delegates in-process to install-engine installRuntimeArtifacts /
uninstallRuntimeArtifacts (the SAME engine functions bin/install.js uses), so
output is byte-identical to today's install (gated by golden-install-parity).
Module-ref call style = monkeypatch-friendly for tests. Lossy by design:
projects files, does not drive the loop (that's the imperative adapter, AC2).
- tests/adapter-declarative-equivalence.test.cjs: kind classification (all 16
runtimes), install/uninstall delegation with exact args (the byte-identity
link), fail-closed construction (missing/invalid runtime throws).
Purely additive — no install.js/install-engine changes. Unblocks AC2 (imperative
adapter) + AC3/AC4 (model/hook/state seams) as follow-up slices.
* chore(changeset): add Changed fragment for declarative embedding adapter (#1680)
* fix(lint): ignore tsc-emitted embedding-adapter/adapter-declarative .cjs (ADR-457)
The new src/embedding-adapter.cts + src/adapter-declarative.cts modules' emitted
gsd-core/bin/lib/*.cjs artifacts must join the ADR-457 ignores list (lint the
src/*.cts source, not the emitted .cjs). Without this, the .cjs is linted under
js.recommended where @typescript-eslint/no-require-imports is undefined, so the
verbatim-copied eslint-disable directive surfaces as 'Definition for rule not
found' — failing the lint-tests CI job.
Also restores the clean line-level disable in adapter-declarative.cts (valid in
the .cts source context where the rule IS defined).
* fix(docs): add adapter-declarative + embedding-adapter to INVENTORY-MANIFEST cli_modules
The new ADR-1239 Phase C-1 modules' built .cjs artifacts must be registered in
docs/INVENTORY-MANIFEST.json's cli_modules array or the
'docs/INVENTORY-MANIFEST.json matches the filesystem' drift test fails on CI.
Mirrors the existing sorted entries.
Extract readModifyWriteStateMd's post-sync preservation block into a pure,
field-classification-table-driven applyStatePreservation in the STATE.md
Transition Module. progress / status / stopped_at now join current_phase_name
as table-governed (getFieldClassification), so a preservation-policy change is
a one-row table edit instead of a per-call-site patch.
This realizes the consolidation ADR-1769 / CONTEXT.md already claimed shipped
('Absorbs readModifyWriteStateMd post-sync preservation block') and routes the
#1264 preservation policy through the single field-classification table — the
bug class is now structurally guarded by the table, not just the call-site
shouldResync flag.
Behavior is byte-identical to the pre-amendment inline block (Hyrum-safe — the
15 readModifyWriteStateMd callers' observable preservation is unchanged):
- state/frontmatter/transition + bug regression suite: 847 pass
- phase/milestone/verify (other RMW consumers): 582 pass
- codex (gpt-5.5/high) adversarial review: CLEAN (58,564-case equiv sweep)
ADR-1769 amendment appended documenting #1796.
Closes#1796
Migrate cmdStateSync, cmdStatePrune, cmdStateUpdate (state.cts) onto the
STATE.md Transition Module substrate and close the maintenance bug pair
#1760/#1761 (ADR-1769, epic #1769). Completes the substrate: all 10
lifecycle/maintenance transitions now route through transitionCore.
- Add {kind: 'sync'|'prune'|'update'} to StateTransitionIntent with syncCore,
pruneCore, updateCore in src/state-transition.cts.
- updateCore: body-only single-field update (strip/reassemble), mirroring the
pre-migration cmdStateUpdate contract.
- pruneCore: pure content→content section pruning (Decisions / Recently
Completed / resolved Blockers / Performance Metrics rows at or below cutoff),
byte-identical tokenizeHeadings splicing. Adapter owns currentPhase, dry-run,
and STATE-ARCHIVE.md.
- syncCore: body writes (Total Plans in Phase, Progress bar, Last Activity)
given disk-derived numbers. Adapter owns the disk scan + roadmap scope.
- #1760: cmdStatePrune now derives currentPhase from 'Current Phase' OR 'Phase'
(the canonical template emits 'Phase: X of Y'), so prune engages on
template-conformant STATE.md instead of bailing 'Only 0 phases'.
- #1761: cmdStateSync skips the Progress write (percent=null) when a milestone
version is set in frontmatter but the ROADMAP has no versioned heading for it
(milestone cannot be bounded). Projects without a milestone version are
unaffected. Leaves progress untouched rather than silently writing fallback-
derived wrong values.
- Regression: bug-1760 (prune engages on template field) + bug-1761 (sync
leaves progress untouched when unbounded). ADR-1769 marked Accepted.
All 82 transition + 515 regression tests pass.
Closes#1793
Migrate cmdStatePatch (state.cts) onto the STATE.md Transition Module substrate
and close the curated-field clobber bug class #1743/#1695 (ADR-1769, epic #1769).
- Add {kind: 'patch'} to StateTransitionIntent, with patchCore in
src/state-transition.cts. Applies each caller-supplied {field:value} pair via
stateReplaceField over the full content (body + frontmatter), tracking
updated vs. failed. data.updated/data.failed mirror the CLI output shape.
- Collapse cmdStatePatch to a transitionCore dispatch. Field-name validation
(security) and the resync-progress decision stay in the adapter.
- #1695/#1743 fix: extend the #1230 delta heuristic in readModifyWriteStateMd to
the curated current_phase_name, table-driven via
getFieldClassification('current_phase_name').preservation === 'preserve-always'.
When a write does NOT change the body Phase: source line, the curated
frontmatter value wins over syncStateFrontmatter's body re-derivation (which
harvests a wrong parenthetical aside — #1695). begin/planned/complete-phase
rewrite their body Phase line, so the delta does not fire for them and
current_phase_name still advances.
- Regression: bug-1695-state-patch-clobbers-phase-name.test.cjs — unrelated
patch preserves curated current_phase_name; patching the Phase source advances.
All 70 transition + 493 regression tests pass (state/phase/milestone + #905/#397/#3242 lineages).
Closes#1791
Migrate the STATE.md write path inside cmdMilestoneComplete (milestone.cts)
onto the STATE.md Transition Module substrate (ADR-1769, epic #1769).
- Add {kind: 'milestoneComplete'} to StateTransitionIntent, with
milestoneCompleteCore in src/state-transition.cts (consulting the
field-classification table). Owns the closure write: Status
('<version> milestone complete'), Last Activity, Last Activity Description,
a Current Position reset to 'Awaiting next milestone', and an Operator Next
Steps reset pointing at the next-milestone command.
- Collapse the inline STATE.md transform in cmdMilestoneComplete to a
transitionCore dispatch. The adapter retains writeStateMd (lock + steady-state
syncStateFrontmatter post-sync) and resolves the runtime-specific
next-milestone slash command, injecting it via intent.nextMilestoneCommand.
- The two section resets carry their pre-seam allow-adhoc-markdown waivers
(regex semantics pinned by existing tests; pending collectSection #1372).
- realClock.today() is byte-identical to milestone.cts's local today
(both new Date().toISOString().split('T')[0]).
- Characterization tests pin field updates, both section resets (replace +
insert paths), and frontmatter #1255 parity.
All 66 transition + milestone + state tests pass.
Closes#1789
Migrate cmdStatePlannedPhase and cmdStateMilestoneSwitch (state.cts) onto the
STATE.md Transition Module substrate (ADR-1769, epic #1769).
- Add {kind: 'plannedPhase'} and {kind: 'milestoneSwitch'} to
StateTransitionIntent, with plannedPhaseCore and milestoneSwitchCore in
src/state-transition.cts (consulting the field-classification table).
- plannedPhaseCore owns the template-aware Status/Last Activity updates, Total
Plans in Phase, Last Activity Description, and the Current Position section
(via the inlined mutateCurrentPositionForAdvance twin). The adapter keeps the
resync:false readModifyWriteStateMd wrapper (#500 RC1).
- milestoneSwitchCore owns the new-milestone reset: rebuilt frontmatter
(milestone/name/status='planning'/zeroed progress) + Current Position body
reset. The adapter keeps acquireStateLock + platformWriteSync (NOT
readModifyWriteStateMd — milestoneSwitch rebuilds frontmatter directly).
- Collapse both callbacks to transitionCore dispatches. The now-dead
updateCurrentPositionFields helper and KNOWN_STATUS_PATTERNS import are
removed (their behavior lives in the transition module's
mutateCurrentPositionForAdvance).
- Characterization tests pin the template-aware preserve-authored invariant,
Total Plans, Last Activity narrative, Current Position update, and the full
milestone reset (frontmatter + position body + gsd_state_version preserve +
Accumulated Context preserve).
All 58 transition + 177 state + phase + bug-2630/bug-905 regression tests pass.
Closes#1786
Migrate the STATE.md write path inside cmdPhaseComplete (phase.cts) onto the
STATE.md Transition Module substrate (ADR-1769, epic #1769).
- Add {kind: 'completePhase'} to StateTransitionIntent + completePhaseCore in
src/state-transition.cts. Pure body field mutations (Current Phase shape/name,
Status, Current Plan, Last Activity + Description, Completed/Total Phases +
Progress percent), consulting the field-classification table for touched keys.
Roadmap progress injected via a new optional deps.roadmapProvider.
- Collapse the ~90-line inline STATE.md transform in cmdPhaseComplete to a
transitionCore dispatch. The adapter retains updatePerformanceMetricsSection
(section table) + syncStateFrontmatter (disk-scan post-sync) and the
multi-file atomic transaction (STATE is committed with ROADMAP/REQUIREMENTS,
so readModifyWriteStateMd is not used here).
- deriveProgressFromRoadmap/clampPercent/stateReplaceFieldWithFallback move from
the phase.cts call site into the pure core (no circular dep: phase-lifecycle
and state-document don't import state.cts).
- Characterization tests in tests/state-transition.test.cjs pin the field
updates, roadmap progress derivation, #1255 frontmatter parity, and the
'Phase:' fallback. All 44 transition + 193 phase tests pass.
No user-visible behavior change. cmdPhaseComplete CLI output and 'phase complete'
behavior unchanged.
Closes#1784
* refactor(#1771): ADR-1769 Phase 1 — STATE.md Transition Module substrate + beginPhase
Lands the Phase 1 substrate per ADR-1769:
- src/state-transition.cts (new Module):
- Field-classification table (FieldClass enum + FIELD_CLASSIFICATION rows)
- STATE_MD_SECTIONS constants block
- Pure transitionCore(content, intent, deps) dispatch
- beginPhase intent implementation (first-time + #3127 resume paths)
- src/state.cts:cmdStateBeginPhase — collapses ~190 lines to a thin
dispatch onto transitionCore via readModifyWriteStateMd. The lock,
no-op write guard, and #1230 post-sync delta heuristic stay in the
RMW seam; the body-mutation policy moves to transitionCore.
- tests/state-transition.test.cjs (24 tests):
- Substrate invariants (table enum, section constants)
- Characterization: 6 first-time body field updates
- Characterization: 5 #3127 idempotency-guard resume behaviors
- Characterization: 3 Current Position section mutations
- Characterization: Current focus body text line (#1104)
- Property (RULESET.TESTS.property-based-testing): beginPhase status
propagation + FIELD_CLASSIFICATION own-property contract
- Resume Current Position mutation (preserves Plan/Phase/Status)
No external behavior change. Full state.test.cjs regression (177 tests)
plus bug-3127/#3242/#905/#948 pass. Property tests surfaced two
pre-existing quirks (state-document.cjs greedy \s* on whitespace-only
field values; Object.prototype method leakage on FIELD_CLASSIFICATION
lookups for strings like 'toString') — documented in test comments;
fix-out-of-scope for Phase 1.
Closes#1771
* refactor(#1771): ADR-1769 Phase 1 codex review corrections
Addresses 3 blocking findings from codex gpt-5.5/high review:
1. FIELD_CLASSIFICATION shape (state-transition.cts):
- Was flat FieldClass enum (collapsed source + preservation)
- Now two-column {source, preservation} rows per ADR-1769 §4
- Added missing fields verified via Memtrace against
buildStateFrontmatter (state.cts:1633-1653): gsd_state_version,
last_updated, last_activity_desc, progress.{total_phases,
completed_phases, total_plans, completed_plans, percent}
- Field 2-7 preservation dispatch can now consult the table
2. Prototype-pollution hardening (state-transition.cts):
- Table is now Object.freeze(Object.assign(Object.create(null), {...}))
- getFieldClassification() helper uses Object.hasOwn; returns null
for inherited prototype methods (toString/valueOf/__proto__)
- Old code: FIELD_CLASSIFICATION['toString'] returned the function
3. STATE_MD_SECTIONS aligned to canonical template:
- Verified against gsd-core/templates/state.md via Memtrace
- Was: 8 entries including non-template sections (## Session,
## Decisions, ## Operator Next Steps, ## Session Log,
## Roadmap Evolution)
- Now: 6 canonical top-level sections (## Project Reference,
## Current Position, ## Performance Metrics, ## Accumulated
Context, ## Deferred Items, ## Session Continuity)
Also: beginPhase now consults getFieldClassification() per touched
field (codex finding: 'table not consulted by transitionCore').
Unknown fields raise immediately — adding a field without a table
row is caught at runtime.
Repo-hygiene catches from gsd-test (not node --test, which missed
these):
- gsd-core/bin/lib/state-transition.cjs added to eslint.config.mjs
ignore list (ADR-457 tsc-generated)
- docs/INVENTORY-MANIFEST.json regenerated via
node scripts/gen-inventory-manifest.cjs --write
Property test for Object.prototype leakage tightened to verify
getFieldClassification() returns null for toString/valueOf/__proto__.
Ref #1771
* fix(#1771): cast Object.create(null) to satisfy @typescript-eslint/no-unsafe-assignment
ESLint CI failed on src/state-transition.cts:72:14 — Object.create(null)
returns `any`, which leaked through Object.assign to the typed
`FIELD_CLASSIFICATION` declaration. Adding an explicit cast to
`Record<string, FieldClassification>` eliminates the unsafe-assignment
while preserving the null-prototype protection codex review recommended.
gsd-test: 21888/21888 PASS.
* fix(#1771): add 'see #1771' to allow-test-rule exemption per ADR-456
CI lint-allow-test-rule-refs failed: 'New allow-test-rule exemption
without an issue ref — add `see #NNN` per ADR-456'. Updated comment
on tests/state-transition.test.cjs to reference the Phase 1 issue.
* fix(#1771): remove unnecessary allow-test-rule exemption
The exemption was added speculatively. The test file does not use
readFileSync + .includes()/.match()/.startsWith() on source content —
it calls transitionCore() with string literals and verifies results
via stateExtractField() and array .includes() on the updated[] array.
No exemption needed.
* feat(#1517): support custom reviewer instances for /gsd:review
Add a bounded review.reviewer_instances config surface so one model-capable
adapter (e.g. opencode) can run as several independent reviewer identities in a
single /gsd:review pass. Instances participate only via review.default_reviewers,
expand before built-in slugs, are available iff their cli is detected, and a
non-matching entry is a hard error (typo must be loud). >=2 same-cli instances
emit a shared-adapter caveat in REVIEWS.md. Default path with no instances is
byte-for-byte unchanged.
Single-source instance->cli resolution lives in resolveReviewerSelection /
normalizeReviewerInstances (parity-locked in
tests/review-reviewer-instances.test.cjs). cli validated against
KNOWN_REVIEWER_SLUGS only (never arbitrary shell); model/agent opaque, never
shell-interpolated.
Closes#1517
* chore(#1517): backfill changeset pr:1766
---------
Co-authored-by: review-bot <review-bot@gsd>
ADR-1239 Phase B (parent #1679). getDirName was a hand-maintained 15-branch
if-chain mapping each runtime to its local content-rewrite dot-dir. Relocate
those values into a documented runtime.localConfigDir descriptor field; derive
getDirName from registry.runtimes[id].runtime.localConfigDir (fallback .claude).
- 16 capability.json gain runtime.localConfigDir (byte-identical values)
- capability-validator.cjs requires it (non-empty dot-dir); registry regenerated
- docs/reference/capability-manifest.md documents the field + the three
divergent values (copilot=.github, antigravity=.agents, kimi=.kimi-code)
- drift-guard test: golden value map + key-set equality both ways
Byte-identical install output for all 16 runtimes (golden-parity harness #1730).
Closes#1756
Co-authored-by: review-bot <review-bot@gsd>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1754): CLI version-skew detection — warn when a global install shadows project-local GSD
Addresses #1754 (approved-enhancement). Detects when the running gsd-tools.cjs
is outside the project root while a project-local install exists — the shadowing
scenario from #1748 where a stale global canary CLI (retired @gsd-build/sdk)
silently overrides project-local GSD.
Implementation (Node CLI entry-point, not shell snippet — avoids bloating 93
workflow files past their size caps):
- src/cli-skew-check.cts: pure function checkCliSkew({resolvedPath, projectRoot,
projectLocalExists}) → string|null. Compares paths via path.relative; returns
a warning when the resolved CLI is outside the project root AND a project-local
install exists. Includes @gsd-build/sdk removal hint when the path matches.
No I/O (pure), no gsd-sdk literal (avoids bug-2801 lint).
- gsd-core/bin/gsd-tools.cjs: wired at startup via the existing findProjectRoot
resolver. Non-blocking (try/catch; advisory stderr warning, never gates).
- eslint.config.mjs: registers the new ADR-457 generated artifact in the ignores.
- tests: 6-case suite (skew/no-skew/legacy/normalization); all green.
- Golden fixtures regenerated (UPDATE_GOLDEN=1) for the new compiled artifact.
- docs/how-to/update-gsd.md: Diátaxis reference note for the skew warning.
Full suite: 3354 pass, 0 regressions (1 pre-existing local AGENTS.md failure).
lint:ci green.
Closes#1754
* chore(#1754): backfill changeset pr placeholder
* chore(#1754): regenerate INVENTORY-MANIFEST for the new cli-skew-check source module
---------
Co-authored-by: review-bot <review-bot@gsd>
* docs(#956): address adr-phase-coverage findings on the MemPalace capability proposal
Resolves the findings from the adr-phase-coverage audit (matrix + interpreted
gaps posted on #956) by embedding durable decision→phase ownership and the
cross-doc gating item into the proposal, so no deliverable sits ownerless
between phases:
- §15.1 Decision → Phase ownership: every §10 decision + user-facing capability
is the explicit responsibility of one phase. Cross-cutting policies get a
primary owner (D6 onError:skip → P1 manifest-encoded; D3 transport → P2
MCP-primary rendering, P5 CLI-fallback headless).
- §15.2 Dependencies & gating items: Phase 6 is GATED on ADR-857's Migrate
phase (workflows calling loop render-hooks) — recorded as a traced gating
item, not prose. UX-auto + UX-curator have no other wiring surface; if
ADR-857's Migrate is descoped, Phase 6 is formally blocked, not silently
dropped. De-risk: Phases 1–5 ship the full manual-invocation value ahead.
- Phase 3 gate: explicitly verifies the inherited UX-enable surface
(gsd capability enable mempalace + config-set), not just the internal
state resolver.
- Phase 6 gate: names the user-observable automatic surface (a
/gsd-execute-phase run auto-produces MEMORY-RECALL.md at plan:pre, curator
spawns at ship:post) instead of the wiring mechanism.
- §17 open questions: each is traced to the phase whose acceptance must
resolve it (wing-identity→P0, replace-migration→P4, curator-tier→P2,
headless-MCP→P5, phase-6-dep→§15.2 gate, diary-namespacing→P6).
Docs-only (proposal refinement); no runtime change.
Closes#956
* docs(#956): correct the Phase-6 framing — ADR-857 is released, auto-fire is wired and verified
Retracts the 'Phase 6 GATED on ADR-857 Migrate' framing introduced in the prior
commit. ADR-857 (capability system + loop render-hooks + workflow call sites) is
released; the host-loop workflows call loop render-hooks at each canonical point,
so mempalace auto-fires when mempalace.enabled. Verified end-to-end: 'gsd-tools
loop render-hooks plan:pre --raw' with mempalace.enabled:true returns the
mempalace-recall step (capId: mempalace, produces MEMORY-RECALL.md).
- Phase 6 row: 'Loop wiring (shipped via ADR-857)' with the verified gate.
- §15.1 Phase 6 row: wired via shipped ADR-857 infra (no gate).
- §15.2: rewritten from 'Dependencies & gating items' (false premise) to
'Loop wiring status' — documents the released/shipped state + the retraction.
- §17.5: 'Phase-6 dependency' → 'Loop wiring (resolved — shipped)'.
The §15.1 decision→phase ownership matrix, Phase 3 UX-enable gate, and §17
open-question traceability from the prior commit stand (those were accurate).
---------
Co-authored-by: review-bot <review-bot@gsd>
Post-merge coverage-audit follow-ups to epic #1702 (found by an independent
gpt-5.5/high audit + adr-phase-coverage cross-reference). None are CRITICAL —
the 9-rule enforcement shipped and works; these close completeness/integrity
gaps between ADR-1703's promises and the as-built reality.
1. drift-guard bin/install.js scope (ADR-1703 L114-119): the drift guard
covered src/runtime-homes.cts only; the ADR named bin/install.js too. Phase
6's glob expansion made bin/install.js a covered surface. Extended
tests/portability-vocab-drift.test.cjs with TWO sound checks: (a) any
bin/install.js top-level function that directly returns path.*() must be in
PATH_RETURNING_FNS (tight, 0 FP — the body-contains heuristic is unsound
here, ~33 FPs); (b) a curated two-way existence lock on the installer path
helpers (catches a rename making a vocab entry stale; keeps the curation in
sync with PATH_RETURNING_FNS). The residual new-resolver boundary (temp-var
shape) is documented.
2. ci-test-scope wiring (the Phase 6 portability selection rule was
ineffective): eslint-rules/ was not in the product-code prefix list, so an
eslint-rules-only change set code_changed=false and CLEARED the matched
tests (reproduced: targeted_tests=[]). Added eslint-rules/ to the prefix
list and the P1-P4 RuleTester suites to the selection rule (it previously
listed only P5/P6). Verified: code_changed=true, 11 tests selected.
3. ADR-1703 acceptance note amended to record the two further as-built
divergences: the disable-ban shipped as an out-of-band test (not the
specified local/no-portability-disable meta-rule — the test runs outside
ESLint so it cannot be self-disabled, at least as strong); and the
drift-guard bin/install.js scope resolution above.
Epic #1702 all eight phase boxes now checked. No runtime change; no-changelog
(contributor tooling + docs).
Closes#1749
Co-authored-by: review-bot <review-bot@gsd>
ADR-1703 Phase 7 / epic #1702 closeout. The mechanical teardown is already
complete across phases 1-6 (regex scanner retired P3, ratchet deleted P4,
allowlist portability-usage gone, windows-portability-ok comments swept,
every DEFECT.WINDOWS-* predicate rewritten per-phase). This phase delivers the
two remaining ADR-mandated closeout items:
- docs/contributing/adding-a-portability-rule.md: the forward architecture
recipe (Diátaxis Explanation) — the five seams (rule / portability-vocab /
platform-guard / disable-ban / ci-test-scope), the zero-escape-hatch contract,
the shipped-rule catalog, and the step-by-step checklist for adding a new
local/* portability rule.
- docs/adr/1703: Status Proposed -> Accepted (all seven phases shipped); a
Phase 7 as-built note recording the two deviations from the Phase 0 catalog —
Phase 6's rename-only scope decision (#1740) and the codex-review precision
tightening on require-fs-op-fallback.
- docs/contributing/cross-platform-portability-rules.md: cross-link to the new
guide from the rule reference.
No code, no test, no runtime change. Epic #1702 Phase 6 box checked; Phase 7
box + epic closure to follow at PR merge.
Closes#1744
Co-authored-by: review-bot <review-bot@gsd>
* feat(#1740): require-fs-op-fallback production AST rule + Windows transient-lock retry (Phase 6)
ADR-1703 Phase 6 of the cross-platform portability epic (#1702). Adds the
second production-code portability AST rule + the ADR-mandated glob expansion
to bin/install.js and scripts/build-hooks.js.
- eslint-rules/require-fs-op-fallback.cjs: flags an unguarded fs.rename /
fs.renameSync (the atomic-publish primitive named first in
DEFECT.WINDOWS-FS-OPS.symptom) that is NOT inside a try/catch whose handler
references a transient errno ('EPERM'/'EBUSY'/'EACCES' or a *RETRY_ERRNOS
set) AND NOT behind a Windows platform guard. A catch that silently swallows
or cleans-up-and-rethrows without an errno check does NOT satisfy the
defect's 'never silently swallow' clause. copyFile/unlink are deliberately
not flagged (they are the fallback primitives per the defect's own
fix-forward). Scope narrowed to rename per Phase 5's precision discipline;
documented on #1740.
- src/shell-command-projection.cts: export retryRenameSync(from, to) — the
drop-in bounded-retry helper over the existing atomicRenameWithRetry.
- 27 bare fs.renameSync sites across 11 modules routed through retryRenameSync
(capability-lifecycle/lock/source, installer-migrations, milestone, phase,
planning-workspace, roadmap-upgrade, runtime-hooks-surface, state,
workstream). Idempotent on POSIX; resilient to AV/indexer transient locks
on Windows.
- eslint.config.mjs: register rule at error on src/**/*.cts; new focused
portability-rules block covering bin/install.js + scripts/build-hooks.js
(ADR-1703 L124-126 glob expansion — both files are compliant: zero
rename violations).
- tests: 15-case RuleTester suite; portability-rule-disable-ban extended
(PROTECTED_RULES + scans bin/install.js/build-hooks.js with shebang
handling); ci-test-scope portability-lint selection rule.
- CONTEXT.md DEFECT.WINDOWS-FS-OPS predicate rewritten to point at the rule;
docs/contributing/cross-platform-portability-rules.md reference + how-to.
Closes#1740
* chore(#1740): backfill changeset pr:1742
* fix(#1740): tighten require-fs-op-fallback precision (codex review HIGH-1/HIGH-2)
Addresses two false-negative findings from the codex (gpt-5.5/high)
adversarial review of PR #1742:
HIGH-1 — a catch that REFERENCES a transient errno but only rethrows (no
retry/fallback) was marked compliant. The DEFECT.WINDOWS-FS-OPS fix-forward
requires retry, not just recognition. Fix: catchHandlerHasRetrySignal now
requires a loop `continue` backedge OR a `return <call>` delegation; a bare
rethrow is flagged. The misleading `/* retry logic */` valid test is replaced
with a real retry loop, and the rethrow-only shape is added as invalid.
HIGH-2 — the nested-try ancestor walk treated an OUTER errno-catch as
protecting the rename even when an INNER catch intercepted/swallowed the error
(the outer catch is unreachable). Fix: isInsideTransientErrnoTryCatch now stops
at the NEAREST enclosing TryStatement WITH A CATCH HANDLER whose block contains
the rename (try-finally is skipped — it doesn't catch); outer catches are no
longer consulted. The unsound nested-try valid test is converted to invalid,
and a try-finally-skipped valid case is added.
Verified: 17 RuleTester cases pass; zero new production violations (the 27
fixed sites use retryRenameSync; the real retry loops — atomicRenameWithRetry,
capability-ledger/consent, build-hooks — remain compliant via continue/errno);
lint:ci green; disable-ban + vocab-drift green.
---------
Co-authored-by: review-bot <review-bot@gsd>
* feat(#1733): normalize-path-in-content production AST rule (Phase 5)
ADR-1703 Phase 5 — the first production-code rule. local/normalize-path-in-content
(src/**/*.cts, @typescript-eslint/parser): flags a path-returning fn result
(path.basename excluded — returns a separator-less filename) interpolated into an
@-reference / config-dir markdown body without .replace(/\\/g,'/') normalization,
per RULESET.CONTENT-PATH-NORMALIZATION / DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT.
Build-and-assess found the canonical defect site (computePathPrefix) already
compliant and only 1 src/ hit — a false positive (path.basename in a status
message) — eliminated by narrowing (exclude basename; require a real @-ref/
config-dir marker, not bare .md). 0 src/ violations: clean forward-prevention.
The out-of-band disable-ban now scans src/**/*.cts too (typescript-estree) so the
production rule also cannot be eslint-disabled. Registered (error) + PROTECTED_RULES;
CONTEXT.md predicates + how-to doc updated.
- RuleTester suite (26 cases)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1733): add changeset for Windows agent-skills path-leak fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: harden mutation-matrix.cjs stdin read against EAGAIN on non-blocking pipe
scripts/mutation-matrix.cjs read piped stdin via readFileSync(process.stdin.fd).
On macOS libuv marks the stdin pipe fd non-blocking, so a synchronous read can
throw EAGAIN before the writer fills the pipe — intermittently, under heavy CI
shard load — aborting the script (status 2) and flaking mutation-matrix-ratchet.
Replace with readStdinSync(): an fs.readSync loop that retries on EAGAIN (1ms
synchronous Atomics.wait yield), stops on 0-byte/EOF, and rethrows other errors.
Deterministic regression test injects EAGAIN via an fs.readSync monkeypatch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci: re-run golden-install-parity on src/lib + installer changes (close drift guard)
golden-install-parity hashes every installed bin/lib/*.cjs per runtime, so it
must re-run whenever the built lib could change. ci-test-scope selected it for
neither src/** nor installer changes, so a source-only edit (e.g. #1691's
milestone.cts/roadmap.cts) recompiled bin/lib and silently drifted the golden
fixtures past the scoped lane. Add golden-install-parity.test.cjs to both the
'TS runtime sources' and 'installer and package layout' selection rules, with
behavioral regression tests for each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: review-bot <review-bot@gsd>
* refactor(#1734): extract install engine from bin/install.js (ADR-1239 Phase B deep move)
Relocate the runtime-artifact install cluster out of the 12,490-line
bin/install.js into a dedicated src/install-engine.cts -> install-engine.cjs:
installRuntimeArtifacts, uninstallRuntimeArtifacts, installOpencodeFamilySkills,
and their cluster helpers (_copyStaged, snapshot/restore, legacy migration,
GSD-entry pruning, preserve/restoreUserArtifacts, OpenCode-family converters,
USER_OWNED_ARTIFACTS).
- bin/install.js imports the engine and re-exports the moved symbols for
back-compat; getCommitAttribution STAYS in install.js (impure config I/O +
argv explicitConfigDir global) and is injected via a resolveAttribution param.
- 17 test files migrated to import the moved symbols from the engine.
- Bookkeeping: eslint built-artifact ignore, .gitignore, INVENTORY manifest+row,
CONTEXT.md Install Engine Module glossary seam.
Behaviour-preserving: install output is byte-identical for all 16 runtimes
(golden-parity harness #1730) — the only delta is the new install-engine.cjs
file shipping in the installed gsd-core/bin/lib/ tree.
Closes#1734
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1734): backfill changeset PR number (#1735)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: review-bot <review-bot@gsd>
Promotes the verify-time enforcement decision that accreted as four chronological addenda on ADR-550 (#1259/#1279/#1346/#1278) into a single first-class architecture-of-record. ADR-550 keeps the spec-phase contract; this ADR owns the test-tier enforcement producer (locate -> machine-prove-fail-first -> run -> dispose) in src/prohibition-enforcement.cts.
Verified against live src/*.cts: the green AND-gate lives in runProhibitionEnforcement, the empty-file vacuity guard is isNonVacuousNodeTestPass (red-side is isNonVacuousNodeTestRed), and the ADR-857 pointer cites the real 'Verification substrate vs. plug-in tier' section / decision #6.
Closes#1606.
Appends an 'Alternatives considered (recall / representation / packaging side)' addendum to ADR-550: the two NEW spec-phase-side rejections (the withdrawn LLM requirement classifier #652, and a deterministic prohibition-recall engine) plus cross-references to the two already-decided ones (no polarity field on truths — D3; deferred single dispatcher CLI — D7e). Enforcement-side alternatives stay in ADR-1606.
Closes#1607.
* feat(#1708): typed documentation-sourced #853 dispatch-flatten
Graduate the #853 orchestrator-backgrounding decision from a scattered RUNTIME==='codex' prose check to a typed, documentation-sourced engine decision. Adds a backgroundDispatch dispatch sub-axis (sourced per host: codex+cursor documented true, 9 documented false, 5 undocumented), shouldFlattenDispatch(dispatch) (inline UNLESS background && backgroundDispatch, fail-closed), and a gsd_run query dispatch-should-flatten the plan/execute workflows call. Cursor is newly background-eligible per its docs (inline->background) — a documentation-justified behavior change. No RUNTIME-name residue for this decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1708): backgroundDispatch citations in matrix + CONTEXT note
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1708): address review findings on typed dispatch-flatten
Code/adversarial review: convert the manager.md/autonomous.md Compound Action preamble from hardcoded 'On Codex' to FLATTEN-based branching (the handlers already use the query; the preamble contradicted them and was wrong for cursor); make shouldFlattenDispatch null-safe + type-honest (accepts raw 'undocumented' registry values); make backgroundDispatch a required descriptor field (matching its siblings, all 16 carry it); strengthen the config.runtime behavioral test; update the bug-853 prose-pin test + comment. Security review clean; Codex confirmed no fail-open.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#1708): backfill backgroundDispatch in role:runtime test fixtures
Making backgroundDispatch a required descriptor field broke role:runtime fixtures in capability-manifest-version/capability-registry/host-integration-descriptors tests that build a dispatch object without it (caught by full gsd-test, not scoped npm test). Backfill backgroundDispatch:false into the well-formed fixtures; the deliberately-malformed 'required-field' test fixture is left malformed by design.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#1708): update fix-1521 dispatch-gating assertion to the FLATTEN gate
fix-1521 pinned the codex-specific run_in_background prose that #1708 graduated to the typed dispatch-should-flatten/FLATTEN gate. Update its assertions to verify FLATTEN=false gating (not a runtime name) + that the old RUNTIME===codex gate is gone. Caught by full gsd-test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1708): add changeset for typed dispatch-flatten
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1708): remove stray temp PR-body file
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#1708): add issue ref to bug-853 allow-test-rule annotations
ADR-456 requires every allow-test-rule exemption to carry a see #NNN reference; the source-text-is-the-product annotations added when migrating the prose assertions lacked it (lint-tests CI gate). Add (see #1708).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1684): add negotiated host-integration interface module
ADR-1239 Phase A: a pure, additive, no-I/O module exposing PROTOCOL_VERSION, the 8-axis HOST_INTEGRATION_AXES closed vocabulary, the UNDOCUMENTED fail-closed sentinel, negotiateHostCapabilities (effective subset of host-declared and engine-known), a typed degradation ladder, and host-capability profiles.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1684): validate and document host-integration axes (16 runtimes)
Extend validateRuntimeBody to validate the 8 hostIntegration axes (closed enums + undocumented sentinel + dispatch struct + reserved-key guards) and the widened runtime vocabulary; author a documentation-sourced hostIntegration block in all 16 runtime descriptors; regenerate the registry. Every per-CLI value is documented (cited) or the explicit undocumented sentinel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1684): add host-integration capability matrix and adr amendment
New per-CLI, per-axis citation reference (value/source/evidence for all 16 CLIs); ADR-1239 Phase-A-implemented amendment; CONTEXT.md glossary seam entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1684): harden dispatch negotiation edge cases
Code-review hardening: treat NaN/Infinity maxDepth as missing (fail-closed, +warning); reset nested/background when namedDispatch collapses to false (struct consistency); SAFE_DEFAULTS dispatch floor to read-only; warn on non-finite protocolVersion; symmetric undocumented warnings for dispatch fields. Pure module — no consumers; behaviour fail-closed throughout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1684): register host-integration.cjs in lint-ignore and inventory
New tsc-generated bin/lib artifact: add to the eslint ignore list (ADR-457 — lint the .cts source), regenerate docs/INVENTORY-MANIFEST.json, and add the docs/INVENTORY.md CLI-modules row. Fixes the 3 gsd-test failures (551-eslint-bin-lib-coverage x2 + inventory-manifest-sync).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1684): add changeset fragment for host-integration interface
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1684): add how-to for sourcing a host's integration axes
Diataxis how-to guide for adding/updating a host's runtime.hostIntegration axes from authoritative docs, the undocumented-sentinel rule, validation, and extending the closed vocabulary. Completes the Step-5 doc quadrants (reference + explanation + how-to). Indexed in docs/README.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1650): fix stale opencode install-path claim in core settings
* feat(#1688): warn on stale model bake for static-frontmatter runtimes
* chore(#1688): backfill changeset pr field with real PR number
* test(#1688): make resolveAgentDir assertions use path.join for windows
* docs(#1688): codify windows path-literal-in-assert anti-pattern + align test
Append a dated amendment to ADR-1508 (append-only convention) recording that
the Runtime Artifact Conversion Module decision is implemented on next: the
content-rewrite engine + walkers + computePathPrefix live behind the deep seam
rewriteStagedSkillBodies/rewriteStagedCommandBodies, the getInstallExports relay
and GSD_TEST_MODE require are deleted, and CONTEXT.md marks the module SHIPPED.
Status stays Accepted (no Implemented status per docs/adr/README.md). Two
deferred follow-ups are tracked as sub-issues of the epic (neither a blocker):
- #1675 dedup convertClaudeToAugmentMarkdown family
- #1676 fast-check property test ($HOME-collapse + idempotency)
Delivery verified by a Codex (gpt-5.4, high) read-only review against the
epic's stated deliverables, cross-checked against the indexed code graph.
Closes#1507
Phase 0 of the Dynamic Context Management epic (#1671): the ADR (documentation) plus a non-shipping reference example under examples/dynamic-context-management/ — the Option-E predicate fact-store parser/selector, a self-contained --check/--write/--select index generator, a sample index, and a runnable demo.
The example is intentionally excluded from 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. Production implementation lands in a later phase.
Resolves#1672
Refs #1671
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#323): fish-shell support in post-install PATH suggestion
Two additive changes to the post-install PATH-suggestion seam, both scoped
to existing functions.
A. Projection: add a fish entry to the persist-mode shell-action list in
projectPathActionProjection() (src/shell-command-projection.cts). fish has
no `export`/`$PATH`-list syntax, so the existing zsh/bash `export PATH=...`
commands are inert when pasted. The new entry emits the fish-native
`fish_add_path '<dir>'` (fish 3.2+, persists via the universal-variable
store, de-duplicating). The directory is single-quoted with the same POSIX
literal escaping as the zsh/bash siblings; verified round-tripping through
real fish 3.7.0 for paths containing quotes, spaces, `$`, `*`, backticks
and unicode.
B. Detection: add homePathCoveredByFishConfig() in bin/install.js, called
from maybeSuggestPathExport() alongside homePathCoveredByRc(). fish does
not use sh-style `export PATH=` rc files, so a fish user whose
fish_user_paths already covers the global bin would otherwise get a
false-positive "not on your PATH" warning on every install. Two
side-effect-free detection routes (no fish subprocess):
1. The universal-variable store (~/.config/fish/fish_variables). fish
serializes this with `full_escape`: every byte outside [A-Za-z0-9/_]
becomes `\xHH` (space -> \x20, `-` -> \x2d, `.` -> \x2e, `$` -> \x24,
unicode -> \uXXXX) and list elements are joined by the literal 4-char
token `\x1e` (NOT a raw 0x1e byte). The detector splits on `\x1e`,
decodes the escapes, then compares each as an absolute literal — a
decoded `$` is part of the directory name, not an unexpanded variable.
Verified against real fish 3.7.0 output.
2. config.fish (`fish_add_path`, `set -gx PATH`, `set -Ux fish_user_paths`)
— plain shell tokens: HOME forms ($HOME/${HOME}/~) are expanded and a
token still holding `$` (e.g. `$PATH`, `$fish_user_paths`) is skipped.
Honours $XDG_CONFIG_HOME and always also checks ~/.config/fish.
No behaviour change for bash/zsh/PowerShell/cmd/Git-Bash users: their entries
and command strings are unchanged; the fish entry is additive and the fish
detector only narrows the set of cases that warn.
Tests: update the projection length assertion (2 -> 3) and fish escaping in
bug-3441; add fish detection + suppression cases in install-path-detection
(uvar store with real fish escaping, dot/hyphen/space/$-literal decode
regressions, config.fish routes, commented-out, relative-segment guard,
unreadable-file fault injection, suppression and emission via
maybeSuggestPathExport).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(changeset): add Changed fragment for #323 fish PATH support (#727)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#323): address review — action-only fish docs, decoder property test, win32 guard
Addresses @trek-e's review on #727:
- docs (blocker): keep the how-to action-only (Diátaxis). Drop the
`# fish — persists via …` comment and the internal-mechanism clause
naming fish_variables/config.fish; leave one command + the exec-fish
directive.
- tests (minor): extract decodeFishUniversalValue to a pure, exported
module function and add fast-check round-trip properties
(decode(fishEscape(p)) === p over arbitrary unicode, abs-path variant,
totality). Consolidated into install-path-detection.test.cjs to respect
the install test-file-count ratchet.
- tests (follow-up): port #721's win32 negative-projection test (no fish
action on win32; persist projection is PowerShell/cmd.exe/Git Bash).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#323): address review — drop unused 'after' import, clarify escaping comment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
* fix(#1577): isolate WebFetch/WebSearch ingress + opt-in injection blocking
Split A of #1573 (security-critical). Scans WebFetch/WebSearch output (the
largest untrusted channel) in gsd-read-injection-scanner; shared
untrusted-input-boundary reference @-included by the 8 ingest agents
(randomized per-wrap delimiters, in-prompt self-scan guard, task-anchoring);
opt-in security.injection_blocking (default advisory — non-breaking).
arXiv: 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472 (Referencing), 2503.00061 (defense-in-depth).
* fix(#1577): address review — honest blocking docs, config key, ADR, property test, revert localized
- A1: rewrote the opt-in-blocking doc + Security changeset honestly — the PostToolUse hook is a
circuit-breaker (halts the agent's next step), NOT a redactor; it does not scrub content already
in the transcript. The prompt-level data/instruction boundary is the primary control.
- A2: registered security.injection_blocking in the config schema + defaults manifests (default
false) + an e2e config-roundtrip test; the dotted setter writes the nested shape the hook reads.
- A3: reverted the 4 hand-edited localized security-model.md (canonical EN only, per convention).
- A5: ADR-1577 (untrusted-input boundary + opt-in blocking; redaction-vs-circuit-breaker rationale).
- A6: property test — scanner never crashes / only emits valid JSON on unicode/large/malformed input.
- Also: inventory (untrusted-input-boundary.md) + agent-size baseline (8 ingest agents) +
drift-guard matcher update (Read -> Read|WebFetch|WebSearch). A7 (content<20 early-exit) left as
the noted pre-existing follow-up.
* fix(#1577): allowlist untrusted-input-boundary.md in injection-scan CI gate
The new reference quotes injection phrases ('ignore previous instructions',
'you are now…') as examples agents must NOT comply with, tripping the repo's
own prompt-injection-scan.sh diff gate (the standalone 'security' CI job, red
on HEAD). Allowlist it alongside the other security docs (security-model.md,
TEST-EXAMPLES.md) that legitimately demonstrate injection patterns. The JS
scanner test doesn't scan references/, so only the shell gate needed it.
Verified: scan --diff origin/next -> 0 findings; scanner JS test 15/15.
* fix(#1577): cover AC #2's gsd-ui-researcher + gsd-assumptions-analyzer
trek-e Major 1: the @-included set dropped two AC #2 agents. Restore them so
no named web-ingress agent is uncovered, keeping the two justified additions
(gsd-ai-researcher, gsd-domain-researcher). Final set = AC's 8 + 2 = 10.
- gsd-ui-researcher carries the full WebSearch/WebFetch + MCP-fetch toolset.
- gsd-assumptions-analyzer reads 5-15 codebase source files (external/source-
document ingress per the boundary), though it has no web tools.
INGEST_AGENTS in the isolation test now asserts all 10; size baselines
regenerated (+60 bytes each, both well under the DEFAULT cap); changeset
reworded 8 -> 10.
Verified: untrusted-input-isolation 14/14; agent-size-budget 39/39.
* docs(#1577): document security.injection_blocking + boundary seam
trek-e Major 2 + Minor:
- docs/CONFIGURATION.md: add the top-level security.injection_blocking key to
the Full Schema and a Security Settings subsection, distinguishing it from
the workflow.security_* namespace; honest circuit-breaker-not-redactor
framing matching ADR-1577 / security-model.
- CONTEXT.md: add the 'Untrusted-input boundary' seam glossary entry.
Verified: lint:docs ok; config-field-docs + contributor-standards green.
* test(#1577): make read-injection property test git-text, not binary
trek-e nit (and more): the file embedded a raw U+FFFF AND a raw NUL byte as
degenerate-edge inputs. The NUL is what actually made git classify it binary
(git binary = NUL in first 8K). Replace both with text-safe escapes that keep
the identical runtime values: '\\x00' and String.fromCodePoint(0xFFFF). File
now diffs/blames line-by-line.
Verified: property test 2/2; no NUL/raw-noncharacter bytes remain.
* docs(#1577): align untrusted boundary docs
Name all 10 ingress agents in INVENTORY/security-model and allowlist the intentional read-injection property corpus for the prompt-injection scanner.
* docs(#1577): align ADR ingest agent count
Update ADR-1577 from 8 to 10 ingest agents so it matches the actual boundary include set and the rest of the docs.
---------
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
* refactor(#1646): route capability routers through Command Routing Hub per ADR-959
Phase 2 of parent #1641. Converts graphify, intel, and audit command
routers from hand-rolled if/else dispatch to routeHubCommandFamily,
implementing the ADR-959 §III(B) line 75 mandate. The three routers
now share the uniform dispatch shape with the 14 host routers.
src/cjs-command-router-adapter.cts
* Imported ERROR_REASON from io.cjs.
* UnknownCommand translation now passes ERROR_REASON.SDK_UNKNOWN_COMMAND
as the second arg to error() — additive for host routers (their
existing one-arg error callbacks ignore the second arg), required
for capability routers whose tests assert reason === 'sdk_unknown_command'
on the JSON-error envelope.
src/graphify-command-router.cts
* Replaced 4-branch if/else with routeHubCommandFamily + handlers map.
* Validation handlers (missing term, missing/invalid --budget) now
return makeInvalidArgs(arg, reason, ERROR_REASON.USAGE) Results
instead of calling error() directly (Q2=C, Q4=ii from grilling).
* Success handlers keep direct output() calls.
* Subcommands array is alphabetical for byte-identical 'Available:'
text in the unknown-subcommand message.
* The unknown-subcommand path is now owned by the Hub's manifest
check (the adapter passes SDK_UNKNOWN_COMMAND).
src/intel-command-router.cts
* Replaced 9-branch if/else with routeHubCommandFamily + handlers map.
* Validation handlers (missing term, missing filePath for patch-meta
and extract-exports) return makeInvalidArgs Results.
* Preserved the timeAgo mutation in the non-raw status handler.
* Preserved the lazy require('./intel.cjs') inside the route function.
src/audit-command-router.cts
* routeAuditUat: routes through the Hub with a synthetic 'run'
defaultSubcommand (no real subcommands). Gives uniform observability.
* routeAuditOpen: captures --json in a closure, strips it from args
before Hub dispatch (so it isn't mistaken for a subcommand by the
manifest check), then branches on wantJson inside the handler to
preserve the formatAuditReport success-path quirk.
docs/CONFIGURATION.md
* Observability section: noted capability commands (graphify, intel,
audit-uat, audit-open) now emit DispatchEvent records since #1646.
.changeset/capability-routers-via-hub.md
* Changed fragment describing the user-visible audit-trail expansion.
pr:0 placeholder will be backfilled after gh pr create returns the
real PR number (DEFECT.CHANGESET-PR-FIELD-DRIFT).
Verification
* graphify cutover tests: 119/119 pass (all unit, dispatch, behavior,
error path, JSON-errors, and registry assertions)
* intel cutover tests: 39/39 pass
* audit cutover tests: 24/24 pass
* bug-974-graphify-budget-missing-value regression test: pass
* npm run test:unit (full suite): 2384 tests, 0 fail
* gsd-test-summary on docker: outcome=passed, 0 failures
(RULESET.PR-FLOW.docker-before-push)
JSON-error envelope parity verified byte-identical: reason values
('usage', 'sdk_unknown_command') and message texts are preserved
across all three routers' error paths.
* chore(#1646): backfill changeset pr: 1647 (DEFECT.CHANGESET-PR-FIELD-DRIFT)
* fix(#1634): honor capability hook matcher and node-prefix command
Capability hook install (applyCapabilitySharedEdits) wrote each settings.json
hook entry with no `matcher`, so a tool-scoped hook fired on every tool (a
fail-closed guard could then block the whole session), and emitted a bare
single-quoted script path so a .js-family hook from a git/tarball source
without +x failed with Permission denied on every matching call.
- Pass through an optional declared `matcher` (entry-level sibling of `hooks`);
absent => omitted (match-all), so existing shipped capabilities are unchanged.
- Validate `matcher` in the declaration (non-empty string, no control chars).
- Emit `node <quoted-path>` for .js/.cjs/.mjs hooks (mirrors first-party);
.sh and others keep the bare quoted path (unchanged).
Root cause: the manifest hook schema (validator rule C4) was {event, script}
only with no matcher, and applyCapabilitySharedEdits never read or wrote one;
the command used shellSingleQuote(absScript) with no node prefix.
Regression tests fail-first on both defects (matcher dropped; bare path) and
pass after the fix; #1460 command assertions updated for the node prefix.
* chore(#1634): backfill changeset pr:1638
* fix(#1634): resolve lint and windows CI failures
- validator: replace the control-character range regex with a char-code loop.
The literal /[\x00-\x1f\x7f]/ tripped ESLint's no-control-regex rule; char
codes are equally precise and lint-clean. Behavior unchanged (still rejects
matchers containing ASCII control characters incl. DEL).
- test: gate the executable-bit precondition on POSIX. Windows fs does not
honor POSIX write modes (a 0o644 write reads back as 0o666), so the
precondition is meaningless there and failed the windows-latest lane. The
node-prefix assertion — the actual fix — is platform-independent and still
runs everywhere.
* docs(#1634): amend ADR-894 for optional lifecycle hook matcher
The `role: "feature"` `hooks[]` entry now carries an optional `matcher`
(settings.json tool-scoping pattern: exact/pipe/wildcard/regex). Document
the field in the §2 schema table and record a Grilling-amendments entry:
the install path projects a declared matcher onto the emitted settings.json
hook entry (absent = match-all, so shipped capabilities are unchanged), and
per-runtime matcher projection (ADR-857 D8) stays a separate concern. This
amendment ships with the fix that introduced the field rather than as a
follow-up.
* docs(#1634): record WINDOWS-POSIX-MODE-BIT-ASSERT defect in CONTEXT.md
Capture the CI failure pattern from #1634/PR #1638 so it is not repeated: a
test that writes a file with a POSIX mode and then asserts statSync().mode
& 0o777 === <octal> passes on macOS/Linux but fails on windows-latest
(Windows fs does not honor POSIX write modes — reads back 0o666). Added as a
machine-greppable DEFECT predicate (symptom/examples/detect/fix-forward/
prevention) next to DEFECT.WINDOWS-TEST-PORTABILITY, with the fix-forward:
gate the mode-bit precondition on process.platform !== 'win32' and keep the
platform-independent behavioral assertion running everywhere.
Two changes to ADR-0174 §5 (Sync dispatch with tight-typed Result<T>):
1. Reconcile the documented Result type to the as-built code. The
original §5 text planned 'Unknown' / 'BadArgs' / 'ValidationFailed'
/ 'NotImplemented' / 'HandlerFailed'; the SDK retirement migration
kept the ADR-0012 names (UnknownCommand / InvalidArgs / HandlerFailure)
and never added the planned ValidationFailed or NotImplemented
variants. HandlerRefusal was added during implementation but never
back-filled into this ADR. The ADR now documents what consumers
actually depend on.
2. Add the optional exitReason?: string field on the InvalidArgs variant
(and update the makeInvalidArgs factory signature). This carries an
ERROR_REASON enum value separately from the existing reason
explanation text, so capability routers migrating from direct
error(msg, ERROR_REASON.USAGE) calls to makeInvalidArgs(...) Results
preserve ERROR_REASON granularity through the Hub Result →
error(msg, exitReason) translation. The field is additive and
backward-compatible.
Also tightens the amendment requirement: 'Adding a new variant OR
adding a field to an existing variant requires amending this ADR.'
Phase 0 of parent #1641. No code changes; pure ADR amendment.
workflow.security_block_on was documented as the minimum threat severity
that blocks advancement, but threats carried no severity and the auditor's
threats_open count (the SECURITY.md gate field) counted every open threat
regardless of severity — so the threshold had no effect, and the auditor's
block_on vocabulary (open/unregistered/none) did not even match the config
enum (critical/high/medium/low/none).
- planner: add a Severity column to the STRIDE threat register; assign
severity per threat.
- auditor: read severity; reconcile the <config> block_on domain to the
severity enum; redefine threats_open as the count of OPEN threats whose
severity is at or above block_on (none => 0). Below-threshold opens are
reported as non-blocking and excluded from threats_open.
- SECURITY.md template + planning-config.md reconciled.
No gate-check site changed: threats_open == 0 stays the gate everywhere;
only its computation is now severity-filtered.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
workflow.security_asvs_level was display-only — the planner hardcoded
'mitigate if ASVS L1 requires it' and the auditor only echoed the level,
so L2/L3 behaved identically to L1.
- New reference gsd-core/references/security-asvs-levels.md defines L1
(opportunistic), L2 (standard), L3 (comprehensive) for both planner
threat disposition and auditor verification depth (higher = superset).
- planner: disposition now scales with the configured ASVS level (no
hardcoded L1) + @-pointer to the reference.
- auditor: verification depth scales with asvs_level (L1 grep-presence,
L2 boundary/vector check, L3 end-to-end trace + bypass check).
- planning-config.md + INVENTORY updated; planner kept under its 48K cap
by extracting the goal-backward worked example to planner-guidance.md.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Resolved conflict in docs/reference/skill-mapping-matrix.md: kept next's Antigravity row (flat layout per #1614, doc-fixed by #1617) AND kept HEAD's Windsurf row (workflows layout per #1615). Also updated the Structural Facts section counts that the original #1615 PR left stale: '15 skill-bearing runtimes' → '14' (Windsurf no longer skill-bearing), 'Nine runtimes stay flat' → 'Eight', removed windsurf from the 'FLAT (unconfirmed)' row in the loader-verification table.
Add an optional structured `coverage:` block to SUMMARY.md frontmatter and a
deterministic classifier that `verify-work` consumes to route deliverables to
auto-pass vs human-UAT — replacing the rejected #1598/#1599 post-hoc heuristic.
- New `src/coverage.cts` (→ bin/lib/coverage.cjs) parses the nested coverage
block (extractFrontmatter can't — its `-` items are scalars-only; this is a
focused parser, sibling of parseMustHavesBlock), validates each entry, and
classifies into auto_passed vs present. Frozen MODE/PRESENT_REASON/ERROR_CODE
typed-IR surface. Exposed via `uat classify-coverage --summary <f>`.
- Auto-pass is the narrow proven case only: strict-boolean human_judgment:false
AND non-empty all-`pass` verification AND zero validation errors. Everything
else — judgment, empty/failing verification, malformed entry — routes to the
human (fail-safe). A malformed block falls back to legacy prose extraction and
surfaces an error; an absent block is byte-identical to pre-#1602.
- execute-plan create_summary populates the block (fail-safe default
human_judgment:true); verify-work extract_tests consumes it; create_uat_file
marks auto-passed entries `source: automated`.
- Templates (summary + 3 variants), CONTEXT.md predicate + glossary, INVENTORY,
eslint/gitignore registration, and Diataxis docs (COMMANDS reference +
USER-GUIDE explanation) updated.
- Behavioral tests via the CLI (no source-grep); parser-robustness regressions
for the null-entry/comment-header/mis-indent cases found in adversarial review.
Closes#1602
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a non-blocking, warn-only codebase-drift gate at plan:pre so a stale
STRUCTURE.md is surfaced before /gsd:plan-phase spawns the planner, instead
of being discovered mid-execution by the existing execute:wave:post gate.
Gated on a dedicated workflow.plan_drift_precheck toggle (default true),
independent of schema_drift_gate. Never blocks planning, never spawns the
mapper agent at plan time.
Review feedback (#1595):
- Use a documented conventional-commit type (feat, not enhance) per
CONTRIBUTING.md / gsd-validate-commit.sh.
- Normalize the plan_drift_precheck command references to the colon prose
form (/gsd:plan-phase, /gsd:map-codebase) to match plan-phase.md §5.65;
registry regenerated from capability.json.
- Make the test temp dirs hermetic: drain mkdtemp dirs in an after() hook
via the helpers.cleanup() budget (local/no-raw-rmsync-in-tests-compliant).
Closes#1592
Claude-Session: https://claude.ai/code/session_016JBiXEAofvB3prJim29XMS
Resolves TBD entries in the skill-mapping matrix provision/consumption
table. All 15 non-Claude platforms assessed as N/A — none have a
documented plugin skill-provision model. File-copy install path
handles skill provision for all runtimes. Updates claude row to
reflect Phase B-provide merged status. Closes epic #1258 acceptance.
Closes#1600
Phase B-provide of epic #1258. Adds a build-generated skills/ dir +
a skills manifest field so plugin-installed GSD exposes gsd-core:<skill>
the native Claude Code way. Closes the gap where plugin-only installs
lacked the skill surface because bin/install.js never ran.
- scripts/gen-plugin-skills.cjs: build step converting commands/gsd/*.md
to skills/gsd-<stem>/SKILL.md via convertClaudeCommandToClaudeSkill
- .claude-plugin/plugin.json: add "skills": "./skills/"
- package.json: add skills to files, gen:plugin-skills to build chain
- tests/issue-766-plugin-manifest.test.cjs: Section H conformance
(manifest field + dir + frontmatter + count parity) + C2 skills symlink
- docs/adr/766-*.md: dated amendment adding skills surface row
- .changeset/rapid-bears-hum.md: type Added
- skills/: 69 generated gsd-<stem>/SKILL.md files (build-committed)
Closes#1596