Commit Graph

4073 Commits

Author SHA1 Message Date
Tom Boucher
2391632973 feat(#1855): add Claude plugin marketplace manifest
Add .claude-plugin/marketplace.json so Claude-plugin-compatible runtimes
(ZCODE et al.) discover gsd-core from a custom marketplace source. The
canonical version lives at plugins[0].version and tracks package.json via
the release version-sync.

Refactor scripts/sync-manifest-versions.cjs so VERSIONED_MANIFESTS entries
are {path, versionKey} dot-path descriptors (default 'version'); register
marketplace.json with versionKey 'plugins.0.version'. getByPath/setByPath
reject __proto__/constructor/prototype (prototype-pollution guard).
plugin.json / gemini-extension.json behavior is unchanged.

- tests/issue-1855-marketplace-manifest.test.cjs: schema + version-sync guard
- tests/issue-844-manifest-version-sync.test.cjs: updated for descriptor shape
- VERSIONING.md + auto-backmerge VERSION_STAMP_MANIFESTS: include marketplace.json
- docs/how-to/install-on-your-runtime.md: marketplace discovery how-to
2026-07-01 11:51:11 -04:00
Tom Boucher
251cfa1f3c fix: docs-exempt + de-dup PR ref on sonnet-5 changeset (adversarial findings)
- Added-type fragment needs docs or a docs-exempt marker (lint-docs-required);
  Sonnet 5 operator docs already landed on next via #1851 → docs-exempt.
- Serializer auto-appends (#pr); drop the manual (#1848) from the body so it
  doesn't render (#1847) (#1848) (#1848). Keep the #1847 issue ref inline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:04:13 -04:00
Tom Boucher
3cc4d1608c test: regenerate golden fixtures + size baselines for the example/doc edits
execute-phase.md and gsd-ai-researcher.md are installed artifacts; their edits
shift install hashes and file sizes. Diff is scoped to those two files' hashes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:04:13 -04:00
Tom Boucher
1bd04e1565 docs(#1847): add Claude Sonnet 5 changeset for next-line changelog + refresh stale model examples
The 1.6.1 forward-port (#1851) used the no-changelog opt-out instead of carrying
a changeset, so Sonnet 5 — unlike every other 1.6.1 fix (#1580/#1591/#1693 whose
fragments live on next) — had no fragment and would be MISSING from the 1.7.0
changelog. Add the fragment so the release render reflects current shipping code.
Also note the bold-checklist form in the #1591 fragment, and refresh two stale
claude-sonnet-4-6 illustrative examples to current IDs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 23:04:13 -04:00
Tom Boucher
d0bdd700c2 chore: regenerate stale gsd-review SKILL.md (pre-existing next drift)
gen:plugin-skills regenerates skills/gsd-review/SKILL.md from source; next's
committed copy was stale (missing the review.default_reviewers 'No flags' block
present in source). Surfaced by the full build during this forward-port. Not
gated by CI (test.yml runs only build:lib), so it had drifted silently.
Deterministic regen; the only generated file out of sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:21:18 -04:00
Tom Boucher
a2a9d38884 test(#1847): regenerate golden-install-parity fixtures for claude-sonnet-5
The sonnet-5 catalog change alters model-catalog.json and settings-advanced.md,
so the per-runtime install-hash fixtures are recaptured (UPDATE_GOLDEN=1). Only
those two file hashes change per runtime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 22:21:18 -04:00
Tom Boucher
bab72fa2b0 fix(#1591): match bold checklist phase markers in isLastPhase fallback
The #1591 checkbox broadening matched `- [ ] Phase N:` but not the
canonical bold form the roadmap template emits (`- [ ] **Phase N: Name**`),
so a <details>-wrapped bold checklist with no next-phase directory still
fell through to is_last_phase=true and a false 'Milestone complete'. Allow
optional **/__ emphasis after the marker and stop the name capture at
emphasis so bold names slug cleanly. Surfaced by adversarial (codex) review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit ad94b38a69)
2026-06-30 22:21:18 -04:00
Tom Boucher
da37986cd0 fix(#1847): resolve standard tier to claude sonnet 5
Point the sonnet/standard tier at Claude Sonnet 5 (`claude-sonnet-5`,
GA 2026-06-30) across the Anthropic-backed runtimes and provider presets,
replacing the superseded `claude-sonnet-4-6`. Mirrors the change into the
CONFIGURATION.md and settings-advanced.md runtime-defaults tables (the
#3229 catalog↔docs parity gate) plus the pt-BR/zh-CN translations, and
updates the tests that pin the old ID. Regenerates the workflow size
baseline for the (smaller) settings-advanced.md.

Scope is Sonnet only — opus/haiku IDs are untouched. Prepared as a 1.6.1
hotfix off the v1.6.0 tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 33260555b4)
2026-06-30 22:21:18 -04:00
Tom Boucher
93e5d2dd84 fix(#1525): skip deferred phases on autonomous reruns (#1846)
* fix(#1525): skip deferred phases on autonomous reruns

* chore(#1525): add changeset fragment

* chore(#1525): fix changeset body

* test(#1525): refresh install parity fixtures

* test(#1525): shrink autonomous workflow

* test(#1525): refresh autonomous baselines

* test(#1525): tolerate Windows temp cleanup flake
2026-06-30 21:29:38 -04:00
Tom Boucher
be54c0dbf5 fix(#1838): exclude backlog 999.x milestone phases (#1843)
* fix(#1838): exclude backlog 999.x milestone phases

* chore(#1838): add changeset fragment
2026-06-30 21:29:35 -04:00
Tom Boucher
88a7500e91 fix(#1836): count prefixed milestone phase dirs (#1844)
* fix(#1836): count prefixed milestone phase dirs

* chore(#1836): add changeset fragment
2026-06-30 20:43:28 -04:00
Tom Boucher
9e32462dd8 fix(#1588): ignore fenced and backlog roadmap phases (#1845)
* fix(#1588): ignore fenced and backlog roadmap phases

* chore(#1588): add changeset fragment

* chore(#1588): fix changeset body

* test(#1588): fold init regression into init suite
2026-06-30 20:43:25 -04:00
Rezolv
8161fcc667 docs(#1609): design note — verifier reach = spec reach (#1715)
* docs(#1609): design note — verifier reach = spec reach

Adds docs/design/verifier-reach.md (new docs/design/ dir): a design-rationale note recording the probe family's organizing principle — a goal-backward verifier only checks assertions that exist, so reliability comes from widening the spec (edge + prohibition probes), not sharpening the verifier. Expands the rationale already stated in ADR-857's verification-substrate section and the CONTEXT.md PROBE.principle predicate. Author's calibration numbers are hedged as internal research, not GSD claims.

Closes #1609.

* docs(#1609): split out plan→execute generalization to keep note within #1609 scope
2026-06-30 17:14:45 -04:00
Behruz Nassre Esfahani
50ff7a8707 fix(#1776): scope prune phase resolution to ## Current Position (#1832)
* fix(#1776): scope prune phase resolution to ## Current Position

cmdStatePrune resolved the current phase by extracting the `Phase` field over
the WHOLE STATE.md body. stateExtractField's fallback chain ends in a pipe-table
match (`| Phase | N |`), so a STATE.md lacking a `Current Phase` field and a
prose `Phase:` line — but carrying an unrelated `Phase`-labelled table row (e.g.
a historical verification table) — resolved that stale table cell as the current
phase and computed a wrong cutoff (bailing "Only N phases" or pruning at a stale
boundary).

Resolve the phase via the same canonical chain buildStateFrontmatter uses —
frontmatter `current_phase` → `Current Phase` field → prose `Phase: X of Y` —
but scope ONLY the prose term to the `## Current Position` section via the
fence-aware locateCurrentPosition seam (new exported sliceCurrentPositionSection).
Frontmatter and the explicit `Current Phase` field stay document-wide (they are
unambiguous); the shared stateExtractField is not narrowed for any other caller.

Tests (folded into tests/state-prune.test.cjs): a stray `| Phase | 2 |` table
with the real phase in frontmatter no longer drives the cutoff (fail-first on
base); template-conformant STATE.md is unchanged; and a fast-check
boundary-containment property that a `| Phase | N |` row outside Current Position
never leaks into the scoped resolution.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1776): add changeset for prune Current Position scoping

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-06-30 13:16:40 -04:00
Behruz Nassre Esfahani
dae7f81482 fix(#1528): drop next-phase guidance from security-blocked verify-work presentation (#1687)
* fix(#1528): drop next-phase guidance from security-blocked verify-work presentation

When security enforcement blocks phase advancement (no SECURITY.md produced),
the verify-work presentation told the user advancement was blocked but still
offered `/gsd:plan-phase {next}` and `/gsd:execute-phase {next}`, competing
with the current-phase fix. Remove those two next-phase lines so the blocked
state routes only to the current-phase resolution (secure-phase, ui-review).
The post-transition presentation — reached only after the completion contract
passes — still offers next-phase planning, which is the correct place for it.

Regression coverage added to tests/ui-review-next-guidance.test.cjs: the
security-blocked block must not offer next-phase actions, and the
post-completion block must still offer them. Regenerated workflow size baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#1528): add changeset for security-blocked next-phase fix

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#1528): recapture golden-install-parity fixtures for verify-work.md change

Rebased onto next; verify-work.md's installed hash changed across all 16
runtime fixtures. Diff confined to the single gsd-core/workflows/verify-work.md
key per runtime. Assert mode 16/16 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-06-30 10:52:30 -04:00
Rezolv
337eee59d5 docs(#1610): ADR for the workflow/agent size-budget ratchet (#1713)
* docs(#1610): ADR for the workflow/agent size-budget ratchet

Gives the already-shipped size-governance decision (epic #1074; PRs #1089/#1096/#1097) its first ADR: per-file LF-normalized byte baseline (anti-creep across every workflow/agent .md) + loose tier hard caps (XL/LARGE/DEFAULT), measured in bytes not lines, with an explicit do-not-game-the-proxy clause (lazy extraction only). Distinct from the install-time skill-surface budget of ADR-0010/0011.

Verified against tests/{workflow,agent}-size-budget.test.cjs + scripts/workflow-size.cjs: cap constants XL_CAP=98304/LARGE_CAP=61440/DEFAULT_CAP=40960/NEW_FILE_CAP=32768, and the largest XL orchestrator is plan-phase.md (~93,973B) ahead of execute-phase.md (~93,426B) — corrected from the draft.

Closes #1610.

* docs(#1610): de-rot tier-cap byte figures — cite baseline JSON not a drifting snapshot

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-06-30 10:39:44 -04:00
Tom Boucher
ec5289a802 Merge pull request #1709 from behruznassre/fix/1698-codex-output-last-message
fix(#1698): capture codex review via --output-last-message, not stdout
2026-06-30 10:37:15 -04:00
Tom Boucher
1f649838b8 Merge branch 'next' into fix/1698-codex-output-last-message 2026-06-30 10:04:59 -04:00
Tom Boucher
03a4ff3987 Merge pull request #1842 from open-gsd/chore/sync-next-version-1.7.0-rc.1
chore: sync next package version to 1.7.0-rc.1
2026-06-30 09:28:58 -04:00
github-actions[bot]
feb7036399 chore: sync next package version to 1.7.0-rc.1 2026-06-30 13:28:49 +00:00
Tom Boucher
b31b562dd2 fix: exclude CHANGELOG.md from golden-install-parity hash manifest (#1840)
CHANGELOG.md contains historical version strings from prior releases.
The PKG_VERSION normalization applied to all files only replaces the
*current* package version, so locally (PKG_VERSION=1.6.0) the normalization
mutates CHANGELOG.md content (1.6.0 appears in old entries), producing a
different hash than in CI (PKG_VERSION=1.7.0-rc.1, which doesn't appear
in CHANGELOG.md). The hash can never match across build contexts.

Add gsd-core/CHANGELOG.md to VOLATILE_FILES so it is excluded from the
parity manifest. It's release documentation — not a functional install
artifact — and changes with every release anyway.

Regenerate all 16 golden fixtures to remove the stale CHANGELOG.md entry
and establish the new baseline.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 00:55:09 -04:00
Tom Boucher
4bdf0fd1cd fix: normalize package version in golden-install-parity hashes (#1837)
The rc release step runs `npm version X.Y.Z-rc.N` before tests, which
rebakes the current version string into hook files and `gsd-core/VERSION`.
Without normalization, every golden parity hash differed post-bump and the
entire test suite failed with 16 golden failures — even though no files
actually changed in a semantically meaningful way.

Add `PKG_VERSION` normalization (`.split(PKG_VERSION).join('<VERSION>')`)
alongside the existing `<HOME>` root normalization so the goldens are
stable across version bumps. Regenerate all 16 fixtures with the new
normalization to establish the new baseline.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 00:11:34 -04:00
Tom Boucher
4f07e9f8de chore: bump rc job timeout-minutes from 10 to 30 (#1834)
npm run test:coverage:unit across 861 test files with coverage
instrumentation runs ~12–15 minutes — the 10-minute ceiling
consistently kills the rc dry-run before tests complete.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 20:42:03 -04:00
Tom Boucher
5e3b7e65a8 fix(#1831): add state-rebuild test files to lint-test-file-count allowlist + fix ESLint errors from #1829/#1830
fix(#1831): add state-rebuild test files to lint-test-file-count allowlist
2026-06-29 20:18:16 -04:00
Tom Boucher
f65866f17d fix(#1831): resolve ESLint errors and unused-var warnings from #1829/#1830
Two hard errors in src/state-transition.cts:
- reconcileCurrentPosition: `!== null` guards on `Record<string,unknown>`
  values don't narrow the type to a primitive, causing
  @typescript-eslint/no-base-to-string to fire on String(fm.current_phase)
  and String(fm.current_phase_name). Extract to typed locals + use typeof
  narrowing so the rule sees string | number — which is the actual invariant.

Four unused-var warnings (warnings are still defects per RULESET):
- stripTemplatePlaceholders: `placeholder` was assigned value.trim() but
  never read — the value came from the loop variable itself, so removed.
- deduplicateSessionArchive: `sectionContent` was sliced but the filter
  below uses the raw hs offsets directly — variable was dead code; removed.
- state-rebuild.test.cjs: first transitionCore call in the orphan-row test
  is covered by the dedicated Leaky-Abstractions guard test below it;
  remove the no-op call rather than silently ignoring its result.
- state-rebuild.test.cjs: `before = state` in the sync regression guard
  test was never read — remove the dead assignment.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 20:08:17 -04:00
Tom Boucher
d8bc1bc636 fix(#1831): add state-rebuild test files to lint-test-file-count allowlist
PRs #1829 and #1830 added tests/state-rebuild.test.cjs and
tests/state-rebuild-cli.test.cjs but did not add them to the
lint-test-file-count allowlist for the 'state' module. The
lint-test-file-count.test.cjs harness reported FAIL_NOVEL_FILES
with the two files listed as novel.

Verified via gsd-test (--base origin/main --head origin/next):
without this fix, 2/22097 tests fail (both lint-test-file-count
allowlist cases). The state module has 17 test files; allowlist
entry now lists all 17 alphabetically.

Process note: the original PRs should have updated this allowlist
in the same commit that added the test files. Recapture of the
golden-install-parity fixtures is NOT required — those fixtures on
next are correct (verified: no diff from UPDATE_GOLDEN=1 locally).
2026-06-29 17:10:55 -04:00
Tom Boucher
bad2c76c5e feat(#1826): cmdStateRebuild CLI + dry-run + verbose + integration tests + docs (#1830)
Phase 2 of approved feature #1817. Wires the pure `rebuildCore` transition
(Phase 1, #1827) to the `gsd state rebuild` CLI subcommand per ADR-1817
§5 (heavy/manual counterpart to lightweight, auto-triggered `state sync`).

Source changes:
- src/state.cts: implement cmdStateRebuild. Locks via
  readModifyWriteStateMd (real path) or read-only (dry-run). Wires
  phaseInventoryProvider to a real .planning/phases/ disk scan (same
  canonical source buildStateFrontmatter uses). --dry-run emits a
  structured preview without writing. --verbose tees the audit-log
  entries to stderr (treated as data-only per ADR-1577).
- src/state-command-router.cts: register cmdStateRebuild in the
  StateModule interface + add the rebuild handler with --dry-run and
  --verbose flag parsing.
- src/command-aliases.cts: register the state.rebuild canonical +
  'state rebuild' alias + mutation=true (so the manifest covers the
  new subcommand for SDK parity / dispatch hub tests).

Tests (tests/state-rebuild-cli.test.cjs, 5 cases):
- state rebuild with no flags reconciles drifted body + drops orphan
  table rows + appends audit log (end-to-end #1, #2, audit log).
- state rebuild --dry-run computes the diff, writes nothing (criterion #5).
- state rebuild --verbose tees the log; audit-log section still written.
- Running rebuild twice on the just-rebuilt file is byte-identical
  (criterion #6 end-to-end).
- Missing STATE.md produces a clean 'STATE.md not found' message, no
  stack trace (CONTRIBUTING QA matrix).

Verified locally:
- node --test tests/state-rebuild-cli.test.cjs → 5/5 pass
- node --test tests/state-rebuild.test.cjs → 18/18 pass (Phase 1 regression)
- node --test tests/state-transition.test.cjs → 85/85 pass (ADR-1769 regression)

Docs (docs/COMMANDS.md): document `state rebuild [--dry-run] [--verbose]`
with the canonical-command block format used by `state sync` /
`state prune`.

Changeset (.changeset/1817-state-rebuild.md): type=Added, user-facing
description of the new subcommand (closes #1817 epic on merge).
2026-06-29 16:15:55 -04:00
Tom Boucher
b5c8a3e590 feat(#1827): rebuildCore + rebuild intent + drift-class unit tests (#1829)
Phase 1 of approved feature #1817. Implements the body-structure
derivability contract landed in ADR-1817 (Phase 0, PR #1828).

Source changes (src/state-transition.cts):
- Add `rebuild` as the 11th intent in StateTransitionIntent (ADR-1769
  transition set extended per ADR-1817 §1).
- Add `phaseInventoryProvider` optional dep + PhaseInventoryRecord type
  (Leaky-Abstractions guard: pure core stays testable without disk I/O;
  rebuild skips table reconciliation when the provider is absent).
- Add `case 'rebuild':` dispatch arm to transitionCore (the missing-case
  compile-time guarantee extends to the 11th case).
- Implement rebuildCore orchestrator + four drift-class helpers per
  ADR-1817 §2:
    * reconcileCurrentPosition — body prose re-derived from frontmatter
    * reconcileByPhaseTable — **By Phase:** table re-derived from disk
      inventory via the new dep
    * stripTemplatePlaceholders — `**Field:** [placeholder]` →
      `**Field:** (pending)` (no canonical source available)
    * deduplicateSessionArchive — keep most-recent 3 archived H3 blocks
- Implement appendRebuildLogSection per ADR-1817 §3 — every mutation
  appends a structured entry (timestamp/kind/section/before/after/reason)
  to `## Rebuild Log`. Idempotency guarantee (ADR-1817 §4): a no-mutation
  rebuild appends NO log entry, so two successive runs on a clean file
  are byte-identical.

Compiled output (gsd-core/bin/lib/state-transition.cjs): regenerated via
`npm run build:lib` (tsc -p tsconfig.build.json).

Tests (tests/state-rebuild.test.cjs, 18 cases):
- Dispatch + idempotency contract (3 tests, including the load-bearing
  'rebuild on a clean file is a no-op' that pins §4).
- Current Position prose reconciliation, criterion #1 (3 tests).
- Template-placeholder removal, criterion #3 (3 tests).
- Session Continuity Archive de-duplication, criterion #4 (4 tests).
- **By Phase:** table reconciliation via phaseInventoryProvider, criterion
  #2 (3 tests, including the Leaky-Abstractions no-op guard).
- Regression guard for sync + prune, criterion #7 (2 tests).

Verified: node --test tests/state-rebuild.test.cjs → 18/18 pass.
Verified: node --test tests/state-transition.test.cjs → 85/85 pass (no
regression on the existing 10 transitions).

Phase 2 (#1826) wires the CLI surface (cmdStateRebuild + --dry-run +
integration tests + docs + changeset). This PR adds the engine only — no
user-visible command yet, so no-changelog label applied.
2026-06-29 15:59:37 -04:00
Tom Boucher
dfff25f1bd docs(#1817): add adr-1817 state.md rebuild derivability contract (#1828)
* chore(context): scrub nul byte from consent-store predicate

CONTEXT.md line 206 (Capability Consent Store predicate) contained a
literal NUL byte between ${realpath(projectRoot)} and <id> documenting
the disk-key join format. The byte was intentional but made the file
binary-detected, breaking grep/rg searches (hit while preparing ADR-1817).

Replace with the 4-char \x00 escape. Preserves the byte-level disk-key
documentation; surrounding prose 'prototype-pollution-safe NUL-joined
keys' carries the semantic context. file(1) now reports 'Unicode text'.

* docs(#1817): add adr-1817 state.md rebuild derivability contract

Phase 0 of approved feature #1817 (epic). Lands the design contract for
the new `rebuild` transition in the STATE.md Transition Module (ADR-1769):

- ADR-1817 (new): `rebuild` is the capstone 11th transition. Six design
  decisions: (1) core substrate, non-toggleable, same tier as the other
  10; (2) section taxonomy — re-derivable (`## Current Position` prose
  from frontmatter, `## By-Phase Progress` table from disk) vs preserved
  (`## Session`, `## Decisions`, unknown sections) vs de-duplicated
  (`## Session Continuity Archive`); (3) orphaned data is logged + dropped
  with a structured audit entry in `## Rebuild Log` (ADR-1411 provenance
  principle); (4) idempotency is a hard guarantee — a no-mutation rebuild
  appends no log entry; (5) non-overlapping scope with `sync` (3
  frontmatter fields, auto-triggered); (6) orthogonal to
  `auto_prune_state` (rebuild reconciles with current canonical sources,
  prune removes by retention policy).

- CONTEXT.md (STATE.md Transition Module section): list `rebuild` as the
  11th intent; add contract predicates mirroring the ADR.

- docs/adr/README.md: add ADR-1817 to the index (Accepted).

Targets the #1776/#1761/#1591 body-drift cluster that survived ADR-1769's
per-field transitions. Phased per ADR-1817: this PR (Phase 0) closes
#1817; Phase 1 (#1827) lands `rebuildCore` + intent dispatch + drift-class
unit tests; Phase 2 (#1826) lands `cmdStateRebuild` CLI + dry-run +
integration tests + docs + changeset.

* docs(#1817): reword state-doctor alternative to satisfy docs-parity lint

The docs-parity-live-registry test scans every docs/*.md (including
docs/adr/) for slash-command tokens and asserts each one resolves to a
live command in the registry. The rejected-alternative #4 in ADR-1817
mentioned a hypothetical `/gsd:state-doctor` workflow, which tripped
the lint (`unknown command token(s): [/gsd:state-doctor]`).

Reword to 'standalone state-doctor workflow' (no slash prefix). The
extractor is aggressive — backticks and space-preceding tokens are both
extracted per the test's own polarity-invariant cases — so the only
sound fix is to not form a slash token at all for hypothetical names.

Verified locally: `node --test tests/docs-parity-live-registry.test.cjs`
now passes 31/31 (was 30/1).
2026-06-29 15:27:22 -04:00
Rezolv
18995380ce feat(#1154): honest verifier — abstain (insufficient_spec) on non-inferable backstop truths (#1738)
* feat(verify-phase): honest verifier — abstain (insufficient_spec) on non-inferable backstop truths (#1154)

Carry the edge-probe's existing `backstop` (non-inferable) tier through the
plan-phase projection as a structured flat-scalar marker instead of a prose
parenthetical, and make verify-phase abstain -> human_needed (never silent-pass)
on a backstop truth it cannot confirm with explicit evidence. Truth-axis mirror
of #644's prohibition judgment-tier (ADR-550 D4).

Engine (deterministic, CI-tested per ADR-550 D5 — never the LLM verdict):
- src/probe-core.cts: truthStatement/truthVerification normalizers, projectTruths
  (conservative serializer), dispositionForUnverifiableTruth (backstop+no-evidence
  -> unverified/flagged/insufficient_spec; backstop+evidence -> green; inferable
  -> green, the over-abstention guard).
- src/roadmap.cts: coerceTruthToString now reads `statement` first so an object-form
  backstop truth is surfaced, not dropped (Hyrum backward-compat for truth-readers).

Workflow/agent/docs: plan-phase emits the structured marker (flat scalar, ADR-550
#1278); verify-phase + gsd-verifier add the abstain arm; new references/honest-verifier.md;
FEATURES/COMMANDS document insufficient_spec; ADR-550 amended (truth-axis D4 mirror).

Decisions adopted (trek-e review): insufficient_spec feeds existing human_needed with a
distinguishable reason (no new VERIFIER_STATUS); changeset Changed; round-trip parity
test; abstain-on-unconfirmed-backstop regression test red-first.

Implementation notes (deviations from the issue's proposed file list, verified live):
- frontmatter.cts needs no change — its flat parser already round-trips object-form truths.
- verify.cts needs no change — it grades artifacts/key_links structurally; truths are
  LLM-graded at the workflow layer, so consumption lives there + the deterministic helper.
- No CJS<->SDK hand-sync — the SDK seam was retired (ADR-0174); src/*.cts is sole source.

Regenerated artifacts: golden-install-parity fixtures, INVENTORY-MANIFEST, size baselines.

* chore(#1154): add changeset (Changed) for honest verifier

User-facing changelog fragment for #1738. Typed `Changed` (not `Added`) per
trek-e review condition 3 — the verify behavior shifts for backstop-bearing specs
(a confident silent `passed` becomes `human_needed`), which is user-visible even
though the schema marker is additive.

* docs(#1154): score-formula also excludes abstained insufficient_spec truths (review nit-1)

trek-e review nit: the verify-phase score sentence said PRESENT_BEHAVIOR_UNVERIFIED
truths were "the only ones excluded" from verified_truths. Post-#1154 an abstained
`insufficient_spec` backstop truth is also excluded (it is not ✓ VERIFIED and routes
to human_needed). Behavior was already correct; this tightens the wording.
Regenerated golden-install-parity fixtures + workflow-size baseline for the touched
verify-phase.md. (Nit-2 — a dedicated insufficient_spec_items frontmatter list — is
intentionally not taken: the current design is ADR-550-D4-conformant, the abstain
cause rides as a distinguishable report reason, and adding it would exceed the
approved scope.)

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-06-29 00:14:32 -04:00
Tom Boucher
ac001be49e fix(#1778): use 1.6 named-flag frontmatter.set form in thread workflow (#1816)
* fix(#1778): use 1.6 named-flag frontmatter.set form in thread workflow

The thread workflow's CLOSE and RESUME branches called frontmatter.set with
the pre-1.6 fully-positional shape (frontmatter.set <file> <field> <value>).
Since 1.6 the dispatcher (gsd-tools.cjs) parses the file positionally and
reads field/value from the named flags --field/--value via parseNamedArgs;
the positional form leaves field/value undefined, cmdFrontmatterSet errors
'file, field, and value required', and the status/updated writes are
silently skipped. Closing a thread never marked it status: resolved and
resuming never marked it status: in_progress.

Switch all four sites (CLOSE status+updated, RESUME status+updated) to the
1.6 hybrid form that verify-work.md already uses:
  frontmatter.set <file> --field <field> --value <value>

Add a regression test with three guards: (1) behavioral — the named-flag
form writes the field while the positional form errors with the documented
message and does not mutate the file; (2) workflow parity — no workflow
under gsd-core/workflows/ emits the positional form, so a future edit that
reintroduces it anywhere fails CI; (3) thread-specific — CLOSE writes
status: resolved and RESUME writes status: in_progress via the named flags.

* docs(#1778): add changeset fragment for thread workflow frontmatter fix

* docs(#1778): fix unclosed inline-code backtick in changeset fragment

* fix(#1778): move regression into owning test + regen baselines

lint-regression-test-names rejects new bug-NNNN-*.test.cjs files; move the
#1778 regression (behavioral named-vs-positional + workflow-parity scan +
thread CLOSE/RESUME assertions) into tests/frontmatter-cli.test.cjs, the
canonical home for frontmatter CLI regressions, and delete the standalone
file. frontmatter-cli.test.cjs already carries the allow-test-rule exemption
for workflow .md content tests.

gsd-core/workflows/thread.md ships to every runtime and is size-tracked, so
recapture the 16 golden-install-parity fixtures (thread.md hash) and the
per-file workflow size baseline (thread.md 12400 -> 12464) via UPDATE_GOLDEN=1
and npm run size:baseline.
2026-06-28 22:42:44 -04:00
Tom Boucher
fd576528a7 fix(#1747): register four search-provider keys in the config schema (#1814)
* fix(#1747): register four search-provider keys in the config schema

buildNewProjectConfig emits seven search-provider availability flags and
research-provider.cts providerAvailability() consumes all seven, but only
three were registered in VALID_CONFIG_KEYS (config-schema.manifest.json).
config-loader.cts then printed an 'unknown config key(s)' warning for the
four unregistered keys (tavily_search, ref_search, perplexity, jina) on
every freshly generated .planning/config.json.

Register the four missing keys in the schema manifest and document them
alongside brave/exa/firecrawl in CONFIGURATION.md. Add a regression test
plus a structural drift guard that requires every config-driven
research-provider flag to be in VALID_CONFIG_KEYS, so a future provider
addition cannot silently reintroduce the drift.

* fix(#1747): move regression into owning test file + add changeset

lint-regression-test-names rejects new bug-NNNN-*.test.cjs files; move the
#1747 regression (four provider keys in VALID_CONFIG_KEYS + provider-flag
drift guard) into tests/bug-2530-valid-config-keys.test.cjs, the canonical
home for VALID_CONFIG_KEYS regressions, and delete the standalone file.

Add the missing .changeset fragment — config-schema.manifest.json lives
under gsd-core/ (user-facing), so changeset-lint requires a fragment.

* test(#1747): regenerate golden-install-parity fixtures for schema change

Adding four provider keys to config-schema.manifest.json shifts its shipped
content hash (65dea848 -> 7d398e94); recapture all 16 runtime fixtures via
UPDATE_GOLDEN=1. Each fixture changes exactly one line — the manifest hash.
2026-06-28 22:42:36 -04:00
Tom Boucher
2d314c3a28 fix(#1772): read full multi-line command in graphify-update hook Gate 2 (#1815)
* fix(#1772): read full multi-line command in graphify-update hook Gate 2

The PostToolUse hook joined tool_name + newline + tool_input.command and
extracted the command with sed -n '2p' — line 2 only. Agent runtimes
(Claude Code's Bash tool among them) routinely emit HEAD-advancing commits
as multi-line scripts ('cd /path', then 'git add', then 'git commit …'), so
line 2 is the 'cd', Gate 2's *"git commit"* match failed, and the rebuild
silently no-op'd on real commits despite graphify.auto_update: true.

Capture line 2 through EOF (sed -n '2,$p') so the case glob sees the full
multi-line command string. Single-line behavior is unchanged (the match
only widens); non-HEAD-advancing multi-line commands still no-op cleanly.

Regression tests cover multi-line commit/merge/pull dispatch plus a
multi-line no-op no-regression guard.

* docs(#1772): add changeset fragment for graphify-update multi-line fix

* test(#1772): regenerate golden-install-parity fixtures for hook change

gsd-graphify-update.sh ships to 9 graphify-aware runtimes; widening the
sed range (2p -> 2,$p) shifts its shipped hash. Recapture the 9 affected
fixtures via UPDATE_GOLDEN=1 — each changes exactly one line (the hook hash).
2026-06-28 22:42:23 -04:00
Tom Boucher
4f6fda852e fix(#1591): phase.complete recognizes checkbox-list phases in isLastPhase fallback (#1819)
* fix(#1591): phase.complete recognizes checkbox-list phases in the isLastPhase fallback

When the active milestone's phase checklist is written as `- [ ] Phase N:`
checkbox items inside a <details> block (the @Azd325 structure) and the next
phase has no directory yet, the disk-based next-phase resolver finds nothing
and phase.complete falls back to the roadmap-enumeration guard at the
isLastPhase site. That guard's phasePattern was heading-only
(/#{2,4}\s*Phase…/), so it never matched checklist items → is_last_phase=true
and next_phase=null on a mid-milestone phase, and STATE.md was wrongly marked
'Milestone complete' with total_phases decremented.

Broaden the marker alternation to match BOTH heading-style (### Phase N:) and
checkbox-list items (- [ ] Phase N: / - [x] Phase N:); the number/name
captures are unchanged. extractCurrentMilestone already surfaces the
<details>-wrapped checklist correctly, so no parser change is needed. The
heading-only sibling patterns elsewhere in phase.cts are left untouched
(scope discipline — only the reproduced isLastPhase fallback is changed).

Regression: a phase complete 36 on a <details>-wrapped v2.0 checklist
(Phases 36-38, only 36 has a dir) returns is_last_phase=false, next_phase=37,
and does NOT flip STATE.md to 'Milestone complete'.

* docs(#1591): add changeset fragment for phase.complete checkbox-list fix

* test(#1752): add total_phases-preservation regression for the #1591 follow-up

#1752 is the scoped follow-up to #1591 — same <details>-wrapped-checkbox
defect, with the additional emphasis on the total_phases decrement cascade.
The #1591 fix (is_last_phase=false) already resolves it: with all 8 phase
dirs on disk, phase.complete 36 on a v2.0 <details> checklist leaves
total_phases at 8 (not decremented to 7) and does not flip STATE.md to
'Milestone complete'. Verified manually before adding the test.

Add the #1752 regression case (8 phase dirs, curated total_phases: 8) to the
phase complete command block in tests/phase.test.cjs, and update the changeset
to reference both issues (#1591, #1752) since this is one user-facing change
resolving both.
2026-06-28 22:41:57 -04:00
Tom Boucher
38c2c1805e fix(#1761): skip conflated progress in state json read-path when milestone unbounded (#1818)
* fix(#1761): skip conflated progress in state json read-path when milestone unbounded

ADR-1769 Phase 7 (#1794) closed the state sync WRITE path — when a milestone
version is asserted in frontmatter but the ROADMAP has no versioned heading
for it, sync leaves Progress untouched. But the state json READ path rebuilds
progress via buildStateFrontmatter, whose roadmapPhaseCount loop counts phase
headings across the WHOLE document when extractCurrentMilestone can't bound
the milestone. state json therefore reported a conflated total_phases (sum of
sibling milestones) + a derived percent — exactly the value the sync guard
was added to prevent. (Repro from the issue: total_phases 8 = 4+4, percent 13.)

Mirror the cmdStateSync guard inside buildStateFrontmatter: when the asserted
milestone cannot be bounded to a versioned ROADMAP heading (the same
versionedHeading test the sync path uses), fall back to the on-disk
phase-dir count for total_phases and skip percent. Bounded milestones
(versioned ROADMAP, or no milestone asserted) are unchanged. The signal rides
on the existing _diskScanCache (new milestoneBounded field) so neither
extractCurrentMilestone's return contract nor its other callers change.

Regression: extend tests/bug-1761-state-sync-wrong-progress.test.cjs with the
read-path case (unbounded → no percent, no conflated total_phases) and a
bounded control (versioned ROADMAP → unchanged percent + total_phases).

* docs(#1761): add changeset fragment for state json read-path fix
2026-06-28 22:41:38 -04:00
Tom Boucher
a47979bb92 refactor(#1679): ADR-1239 Phase B — collapse program + command chains [AC2 slice 4] (#1813)
* refactor(#1679): ADR-1239 Phase B — collapse program + command chains [AC2 slice 4]

Phase 2 AC2 slice 4. Collapses two more duplicated runtime->string chains in
bin/install.js's post-install next-step message:

- program (14 branches): an EXACT duplicate of runtimeLabel -> getRuntimeLabel.
- command (14 branches): the per-runtime /gsd-new-project invocation syntax
  (gemini '/gsd:', codex '$', cursor skill-mention, kimi '/skill:', default
  '/gsd-new-project') -> new getRuntimeNewProjectCommand(runtime) helper.

- src/runtime-name-policy.cts: RUNTIME_NEW_PROJECT_COMMANDS table +
  getRuntimeNewProjectCommand(runtime) (sibling to runtimeFlags/getRuntimeLabel).
- bin/install.js: import getRuntimeNewProjectCommand; replace the program +
  command chains with single lookups.
- tests/runtime-label-policy.test.cjs: 2 new tests for
  getRuntimeNewProjectCommand (4 overrides + default for the other 12).

runtime === count: 53 -> 25 (-28). Cumulative Phase 2 this session: 129 -> 25
(-104). golden-install-parity 16/16 (program/command are stdout-only so not
parity-covered, but program matches RUNTIME_LABELS exactly and command values
are preserved verbatim in the table). AC2 data-collapse now essentially
exhausted; remaining 25 branches are the ADR-1235 agent-loop tail + per-runtime
semantic behavior.

* chore(changeset): add Changed fragment for program+command collapse (#1679)
2026-06-28 15:22:13 -04:00
Tom Boucher
f954bb4cac refactor(#1679): ADR-1239 Phase B — collapse is<Runtime> flag blocks into runtimeFlags [AC2 slice 3] (#1811)
* refactor(#1679): ADR-1239 Phase B — collapse is<Runtime> flag blocks into runtimeFlags [AC2 slice 3]

Phase 2 AC2 slice 3. Collapses the four duplicated 'const isX = runtime === x'
declaration blocks in bin/install.js (uninstall / writeManifest / install / a
fourth helper — 48 of the 101 remaining runtime=== branches) into a single
runtimeFlags(runtime) helper in src/runtime-name-policy.cts, sibling to
getDirName / getRuntimeLabel / getGlobalConfigHomeFragment.

The purest add-a-host tax: a new runtime meant remembering to add ~12 flag lines
to each of four functions. Now it is one entry in RUNTIME_FLAG_IDS.

- src/runtime-name-policy.cts: RUNTIME_FLAG_IDS + runtimeFlags(runtime) -> frozen
  map of is<Runtime> booleans (single runtime=== source, via loop).
- bin/install.js: import runtimeFlags; replace the 4 declaration blocks with one
  destructure each. ZERO usage-site churn (flag names preserved; install.js's
  eslint block has no no-unused-vars rule so destructure-all is clean).
- tests/runtime-flags.test.cjs: 4 tests (each runtime sets exactly its flag,
  claude/unknown/empty -> all false, all 15 flags present + frozen, drift guard).

runtime === count: 101 -> 53 (-48). golden-install-parity 16/16 byte-identical
(behavior-identical collapse). AC2 data-collapse now substantially complete;
ADR-1235 agent-loop tail + per-runtime semantic residue remain (separate).

* chore(changeset): add Changed fragment for runtimeFlags collapse (#1679)
2026-06-28 14:59:29 -04:00
Tom Boucher
41193a44bd feat(#1681): ADR-1239 Phase C-2 — gsd-mcp-server bin entry + lifecycle test [slice 3b] (#1810)
* feat(#1681): ADR-1239 Phase C-2 — gsd-mcp-server bin entry + lifecycle test [slice 3b]

Phase 4 slice 3b (closes #1681). The companion MCP server bin entry so any
MCP-consuming host connects via 'npx gsd-mcp-server' (or its bin on PATH) and
gets GSD command (point 1) + state IO (point 5) with no bespoke plugin.

- gsd-core/bin/gsd-mcp-server.cjs: #!/usr/bin/env node shim requiring
  ./lib/mcp-server.cjs + runServer({stdin, stdout}); non-zero exit on fatal
  error (justified n/no-process-exit disable). Mirrors gsd-tools.cjs.
- package.json: add 'gsd-mcp-server' bin entry.
- tests/gsd-mcp-server-bin.test.cjs: 3 process-lifecycle tests — initialize +
  tools/list round-trip + clean exit, malformed-line -> parse error + server
  keeps running, empty stdin -> clean exit. Synchronous spawnSync (bounded;
  server exits on stdin EOF, no orphan).

Phase 4 trust-gate (#1806) + loader wiring (#1808) + server module (#1809) +
this bin/lifecycle slice = all of #1681's deliverables. Concrete host binding ->
Phase 5 (#1682). npm-integrity + eslint + security + inventory all clean.

* docs(#1681)+chore(changeset): how-to for the companion MCP server + Added fragment

docs/how-to/connect-gsd-mcp-server.md — Diataxis how-to guide for connecting
any MCP-capable host to gsd-mcp-server: goal-oriented flow (add config → restart
→ verify), real-world per-host conditionals, troubleshooting, and a trimmed
reference table. Explanation/reference linked out (ADR-1239, capability-trust-
model) per Diataxis boundary rules rather than mixed in.

.changeset/humble-seals-rest.md — type: Added (first user-reachable surface of
the epic: a new bin command). The how-to doc satisfies the docs-required gate.

* fix(#1681): move gsd-mcp-server shim to top-level bin/ (out of the runtime-copied tree)

The shim at gsd-core/bin/gsd-mcp-server.cjs was inside the tree the installer
copies into every runtime config dir, so it leaked into all 16 runtimes and
broke golden-install-parity. The MCP server is a PACKAGE bin the host spawns
(npx gsd-mcp-server), not a per-runtime artifact — so it belongs at top-level
bin/ alongside install.js (which is also never copied into a runtime config).

- gsd-core/bin/gsd-mcp-server.cjs -> bin/gsd-mcp-server.js (require path now
  ../gsd-core/bin/lib/mcp-server.cjs).
- package.json: bin entry -> bin/gsd-mcp-server.js.
- tests/gsd-mcp-server-bin.test.cjs: SHIM path updated.
- eslint.config.mjs: add bin/gsd-mcp-server.js to the bin/install.js block
  (drops the n/no-process-exit disable — the n plugin isn't loaded for that
  block, so the disable referenced an undefined rule).

golden-install-parity 16/16 restored; lifecycle + unit tests green; eslint 0;
lint:ci all ok.
2026-06-28 14:27:43 -04:00
Tom Boucher
2b38356275 feat(#1681): ADR-1239 Phase C-2 — companion MCP server module (points 1 + 5) [slice 3a] (#1809)
* feat(#1681): ADR-1239 Phase C-2 — companion MCP server module (points 1 + 5) [slice 3a]

Phase 4 slice 3a. A minimal, dependency-free stdio JSON-RPC 2.0 server exposing
two of the six interface points so any MCP-consuming host (Claude/Codex/OpenCode/
VS Code/Gemini/Cursor/Cline/Hermes) can drive GSD with no bespoke plugin:

- point 1 (command): tool gsd_invoke_command -> createHub/dispatch.
- point 5 (state IO): tools gsd_read_state / gsd_write_state -> the Phase 3
  stateIO seam (filesystem default).

- src/mcp-server.cts: handleMessage(request, ctx) pure JSON-RPC handler
  (initialize / tools/list / tools/call) + runServer({input, output}) thin
  line-delimited-JSON loop over injectable streams. 3 tools wired to the
  existing engine surfaces. NO new dependency (hand-rolled JSON-RPC; the repo
  ships only claude-agent-sdk + ws — an MCP SDK is a separate packaging call).
- tests/gsd-mcp-server.test.cjs: 9 tests (initialize, tools/list, state
  read/write round-trip, command dispatch, unknown tool / missing name /
  unknown method / notification / parse error, injectable-stream round-trip).

Bin entry / packaging / manifest-version-sync / process-lifecycle docs ->
slice 3b. This slice ships the importable, tested server surface a host (or the
bin shim) drives. Proactive CI gates: ADR-457 ignores + INVENTORY-MANIFEST +
injection-scan audit. All clean locally (9 tests + security 15/15 + inventory +
eslint 0 problems).

* chore(changeset): add Changed fragment for companion MCP server module (#1681)
2026-06-28 12:45:25 -04:00
Tom Boucher
d2518e142d feat(#1681): ADR-1239 Phase C-2 — wire trust gate into loadRegistry (configHome confinement) [slice 2] (#1808)
* feat(#1681): ADR-1239 Phase C-2 — wire trust gate into loadRegistry (configHome confinement) [slice 2]

Phase 4 slice 2. loadRegistry({includeInstalled:true, configHome}) now rejects
(skip + warn, fail-closed) any installed third-party descriptor whose declared
destSubpath resolves outside the supplied configHome, BEFORE it is composed.

- src/capability-loader.cts: LoadRegistryOptions.configHome?:string (optional,
  backward-compatible). Require external-descriptor-trust.cjs (typed). Before
  overlayCaps.push(cap), if configHome set, assertDescriptorConfined(cap,
  configHome) — on throw, skip('configHome confinement rejected: ...') +
  continue. Fail-closed via the loader's existing per-candidate skip semantics.
- tests/external-descriptor-loader-wiring.test.cjs: integration test — escaping
  overlay skipped with confinement reason when configHome set; confined overlay
  composes; omitted configHome = no load-time check (backward-compatible).

Defense-in-depth with Phase 2: load-time rejects malformed descriptors early
(this slice); install-time assertDestWithinConfigHome bounds actual writes
(#1679 AC3). Existing capability-loader.test.cjs 54/54 (no regression —
additive optional option). Companion MCP server -> slice 3.

* chore(changeset): add Changed fragment for loadRegistry configHome confinement wiring (#1681)
2026-06-28 12:25:02 -04:00
Tom Boucher
21b81ea068 feat(#1681): ADR-1239 Phase C-2 — external-descriptor trust gate (configHome confinement) [slice 1] (#1806)
* feat(#1681): ADR-1239 Phase C-2 — external-descriptor trust gate (configHome confinement) [slice 1]

Phase 4 slice 1. Load-time, fail-closed configHome write-confinement for
installed third-party host-plugin descriptors — defense-in-depth on top of the
existing opt-in/schema/consent/first-party-wins loader gates + Phase 2's
install-time assertDestWithinConfigHome (#1679 AC3).

- src/external-descriptor-trust.cts: isPathConfined(target, root) pure
  cross-platform containment primitive + assertDescriptorConfined(descriptor,
  configHome) — walks runtime.artifactLayout global/local destSubpaths, throws
  fail-closed (naming descriptor + path) on the first escape. Rejects ../escape
  + absolute-outside-root. Missing layout / invalid entries skipped.
- tests/external-descriptor-confinement.test.cjs: 7 tests (containment
  primitive, benign passes, global/local/absolute escapes rejected, missing
  layout, invalid entries).

Load-time twin of Phase 2's install-time gate — rejects malformed/escaping
descriptors BEFORE consent even matters. NOT wired into loadRegistry yet
(slice 2, 4 callers, medium blast radius); this ships the reusable gate + tests.
Not the ADR-1577 prompt-injection breaker (separate concern, shared word trust).

Proactive CI gates: ADR-457 ignores + INVENTORY-MANIFEST + injection-scan audit.
All clean locally (7 tests + security + inventory + eslint 0 problems).

* chore(changeset): add Changed fragment for external-descriptor trust gate (#1681)
2026-06-28 12:00:43 -04:00
Tom Boucher
abd21b2968 feat(#1680): ADR-1239 Phase C-1 — hook-bus + stateIO seams [AC4] (#1805)
* feat(#1680): ADR-1239 Phase C-1 — hook-bus + stateIO seams [AC4]

Phase 3 slice 4 (AC4, final #1680 slice). The last two adapter seams behind
the negotiated hookBus/stateIO axes:

- src/hook-bus.cts: createHookBus({bus}, {hostEmit?}) -> host/engine/none.
  engine = in-process pub/sub (handler errors isolated); host = host-owned,
  fail-closed emit until a host emitter is bound; none = silent no-op (degrade
  to rule-text). PORTABLE_EVENT_FLOOR = SessionStart/PreToolUse/PostToolUse/
  Stop/SessionEnd (the claude dialect all hook hosts share).
- src/state-io.cts: createStateIO({io}, {backend?}) -> filesystem (today's
  behavior — straight fs) / sandboxed-storage / session-log-append (fail-closed
  seams until a host backend is bound).

Proactive CI gates: ADR-457 ignores + INVENTORY-MANIFEST entries for both new
.cjs; injection-scan 'act as' substring audit; unused-import check. All clean
locally (11 tests + security 15/15 + inventory + eslint 0 problems).

Phase 3 (#1680) seam layer now complete. Concrete host binding -> Phase 5
(#1682, D15/D18).

* chore(changeset): add Changed fragment for hook-bus + stateIO seams (#1680)
2026-06-28 11:44:36 -04:00
Tom Boucher
b152f7e64c feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3] (#1804)
* feat(#1680): ADR-1239 Phase C-1 — model adapter seam (passive + active) [AC3]

Phase 3 slice 3 (AC3). Two model-layer adapters selected by the negotiated
modelMode axis (host-integration.cts):

- passive: formalizes today's tier routing from src/model-resolver.cts —
  resolveModel delegates straight to resolveModelForTier (byte-for-behavior).
  This is the CLI runtimes (claude/gemini/codex/opencode/cursor/...): GSD injects
  prompts / a per-agent model field.
- active: a host-supplied sendRequest seam (VS Code vscode.lm / pi providers) —
  GSD calls the model through the host. Ships as a fail-closed seam (throws until
  a provider is bound); Phase 5 wires a concrete provider.

createModelAdapter({modelMode}, {sendRequest?}) — factory gating throws on
invalid mode. Proactive CI gates applied: ADR-457 eslint ignores entry +
INVENTORY-MANIFEST cli_modules entry + comment-wording audited for the
injection-scan substring trap.

* chore(changeset): add Changed fragment for model adapter seam (#1680)
2026-06-28 11:27:44 -04:00
Behruz Nassre Esfahani
32b8c836c3 test(#1698): recapture golden-install-parity fixtures for review.md change
Rebased onto next; review.md's installed hash changed across all 16
runtime fixtures. Diff confined to the single gsd-core/workflows/review.md
key per runtime. Assert mode 16/16 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:17:47 -07:00
Behruz Nassre Esfahani
d858b6faca fix(#1698): use CRLF-safe split in codex-exec flags test
The lint-tests check (local/no-crlf-fragile-split) flagged splitting
readFileSync content on literal "\n". Use .split(/\r?\n/) for Windows
git-autocrlf portability.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:15:37 -07:00
Behruz Nassre Esfahani
f9143a400c chore(#1698): add changeset
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:15:37 -07:00
Behruz Nassre Esfahani
2bada4de1a fix(#1698): capture codex review via --output-last-message, not stdout
The Codex reviewer in review.md captured the review by redirecting codex
exec's stdout to the review file. On Windows, codex writes process-teardown
output to stdout after the final agent message, so that noise was appended
to a non-empty file and slipped past the `[ ! -s ]` empty-output guard as a
silently polluted review (consumed by severity extraction and the
plan-review-convergence gate).

Capture the final message via codex's own `-o/--output-last-message <FILE>`
and discard stdout. The #1115 contract is preserved (stderr to .err,
capability-gated $CODEX_BYPASS_FLAG, --ephemeral, --skip-git-repo-check) and
the empty-output fallback still fires when codex leaves no/empty output.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:15:37 -07:00
Tom Boucher
da368311ea feat(#1680): ADR-1239 Phase C-1 — imperative embedding adapter (composes loadRegistry) [AC2] (#1803)
* 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.
2026-06-28 11:10:59 -04:00
Tom Boucher
c642ed0ec5 feat(#1680): ADR-1239 Phase C-1 — declarative embedding adapter + minimal HostIntegrationInterface [AC1] (#1802)
* 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.
2026-06-28 02:12:06 -04:00
Tom Boucher
4810bfe4df refactor(#1679): ADR-1239 Phase B — collapse getConfigDirFromHome chain into getGlobalConfigHomeFragment [AC2 slice 2/6] (#1801)
* refactor(#1679): ADR-1239 Phase B — collapse getConfigDirFromHome chain into getGlobalConfigHomeFragment

AC2 slice 2. Collapses the 14-branch runtime->global-config-home source-fragment
chain in bin/install.js getConfigDirFromHome (the hook path.join() codegen mapping)
into a single getGlobalConfigHomeFragment(runtime) lookup in runtime-name-policy.cts,
sibling to getDirName / getRuntimeLabel.

The antigravity branch stays dynamic in the caller (resolveAntigravityGlobalDir +
path.relative — env-overridable, multi-segment); the prior inner unreachable
`if (!isGlobal) return "'agents'"` (dead: !isGlobal returns at the fn top) is dropped.

Behavior: byte-identical. Fragments preserved verbatim in the table.
- claude/unknown/empty -> default '.claude' fragment
- 14 runtimes (copilot..kimi) -> table lookup
- antigravity -> dynamic (unchanged)

Verification:
- TDD: tests/global-config-home-fragment.test.cjs (golden map + 2 drift guards +
  fallbacks). Red->green.
- 16-runtime golden install parity: byte-identical (hook codegen output unchanged)
- eslint + test-file-count + regression-names clean
- runtime === count in install.js: 115 -> 101 (-14)

* chore(changeset): add Changed fragment for getConfigDirFromHome collapse (#1679)
2026-06-28 00:26:13 -04:00