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
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).
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
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>
* 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>
* feat(#1724): complete install write-confinement (copyWithPathReplacement, installCodexConfig)
ADR-1239 Phase B (parent #1679). PR #1706 (2a) confined the layout-driven
plan path and _copyStaged's inline guard; this completes the destSubpath
write-confinement acceptance criterion for the two remaining write sites
and canonicalizes _copyStaged.
- copyWithPathReplacement: new required confinementRoot param; a fail-closed
gate (assertDestWithinConfigHome + hasExistingSymlinkBetween) runs BEFORE
the rmSync/mkdirSync; root threaded through recursion + all 4 call sites
(stageRoot for pristine staging, targetDir for the 3 install sites); writes
go through the validated absolute path. Exported for behavioral testing.
- installCodexConfig: confines config.toml, agents/, and per-agent
agents/<name>.toml (name from agent frontmatter) via the canonical gate +
symlink-escape guard (parity with the other two functions).
- _copyStaged: fail-closed when configDir omitted (all callers pass it);
delegates strict-subpath to the canonical gate, keeps its symlink guard,
writes through the validated absolute path.
Reuses the existing assertDestWithinConfigHome (handles absolute dests via
path.resolve) and hasExistingSymlinkBetween — no new module. Behavioral
regression tests (escape/dest==root/fail-closed/symlink/name-injection),
red-first proven; cross-platform symlink tests use t.skip not bare return.
Closes#1724
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1724): backfill changeset PR number (#1725)
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>
* 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>
Three related defects in cmdConfigSet, all 'config-set stores invalid values silently':
1. Missing guards: workflow.security_block_on (enum) and
workflow.security_asvs_level (integer 1-3) had no store-time validation.
2. Systemic JSON-coercion bypass: every string-enum guard used
VALID_X.includes(String(parsedValue)). Because the value is JSON-parsed
before validation, String(["member"]) === "member" let a JSON array
slip through and an array was stored in a scalar key. Reproduced on
human_verify_mode, statusline.context_position, context_guard_mode,
fallow.scope/profile, source_grounding_authority, drift_action, context.
3. Unvalidated capability keys: 32 capability-registry-owned keys (4 enum,
25 boolean, 2 number, 1 string) had no hardcoded guard, so any value —
including coerced arrays/objects and out-of-enum strings like
code_review_depth=garbage — was stored silently.
Fix: a type-safe assertEnumValue() helper (requires typeof === 'string'
before membership), routed through all nine central string-enum guards
(messages preserved byte-for-byte); plus a generic capability-registry
validation block that validates every capability key against its declared
type/values (enum via the registry's values — single source of truth —
boolean, number, string). Behavioral regression tests cover every central
enum key and representative capability keys (array + object coercion
rejected, out-of-enum rejected, valid accepted) with boundary coverage for
the security keys.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The commandName validation tests legitimately contain real injection payloads (newline + system-role override phrases, fake [SYSTEM] tags, jailbreak strings) to prove the validator rejects them. The scanner cannot distinguish a test fixture asserting rejection from an actual injection attempt, so CI failed on the test that adds the security control.
Added tests/windsurf-conversion.test.cjs to scripts/prompt-injection-scan.sh ALLOWLIST with a comment citing the defect class.
Also added DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS to CONTEXT.md so the pattern is documented. Initial draft of that predicate ITSELF triggered the scanner (it quoted the literal injection phrase as an example) — reworded to use descriptive references ('scanner-matching payload', 'instruction-override phrase') since the scanner scans CONTEXT.md too. That meta-collision is now called out in the fix-forward and prevention subkeys.
* enhance(#1549): validate PR-title issue-ref convention at open time
The release changelog is title-driven: release.yml generates "What's Changed"
from PR titles, then format-github-release-notes.cjs buckets each line by its
conventional-commit prefix and relies on a `(#<issue>)` in the title to render
the issue link. Both rules were enforced only socially, so titles like
`fix(core): ...` (no issue link) and `[security] fix(...): ...` (leading tag
defeats the `^fix` bucket anchor -> mis-filed under Enhancement) silently broke
the changelog, landing on the maintainer as release-time cleanup.
Extract the title matcher into one shared module consumed by BOTH the changelog
classifier and a new PR-title CI gate, so a title that passes the gate cannot
mis-bucket in the changelog (single source of truth).
- scripts/lib/conventional-title.cjs (new): classifyBucket + evaluatePrTitle +
the anchored regexes. One matcher, two consumers.
- scripts/release-notes/format-github-release-notes.cjs: classifyTitle now
delegates to classifyBucket (behavior preserved; existing tests green).
- .github/workflows/pr-title-validator.yml (new): runs evaluatePrTitle on
pull_request opened/edited/reopened/synchronize, for ALL authors (the drift
came from member PRs). Trusted base-ref checkout; WARN_ONLY knob for rollout.
- tests/conventional-title.test.cjs (new): bucket + gate cases incl. the
leading-tag mis-bucket (backfills the untested classifyTitle case) and a
cross-check that the classifier delegates to the shared matcher.
- CONTRIBUTING.md: document the `type(#<issue>):` rule and no-leading-tag.
Claude-Session: https://claude.ai/code/session_01UMV5Qr3H4oFikbuiEauGQk
* fix(#1549): check out the PR in pr-title-validator so the new matcher resolves
The workflow checked out the base branch (next) as a trusted policy source, but
the shared matcher (scripts/lib/conventional-title.cjs) is introduced by this PR
and does not exist on next yet — so require() failed and validate-title errored
on its own introducing PR. Check out the PR's merge ref instead: the matcher
under review is present, the check is self-consistent, and a fork pull_request
runs read-only with no secrets, so running the PR's own pure-string regex is
safe.
* fix(#1549): move conventional-title.cjs out of installed scripts/lib/
bin/install.js bundles every file under scripts/lib/ into the user-installed
payload (the changeset CLI's dependencies), and install.test.cjs (#935) asserts
that exact set. The new matcher is release/CI tooling that must NOT ship to
users, so placing it in scripts/lib/ both broke the install manifest test and
would have shipped dead code. Relocate it next to its consumer in
scripts/release-notes/ (which the installer does not copy) and update the three
require paths (classifier, workflow, test) + the CONTRIBUTING reference.
install.test.cjs now 125/125; conventional-title + release-notes suites green;
lint:ci clean.
* fix(#1549): load title matcher from trusted base ref, not PR code
Addresses review (Solvely-Colin + trek-e): the gate checked out the PR
merge ref and require()'d evaluatePrTitle from PR-controlled code, so any
future PR could edit conventional-title.cjs to return { valid: true } and
wave its own malformed title through — a self-bypassable required check.
Load the matcher from a base-branch checkout instead (ref:
github.event.pull_request.base.ref), the same trusted-policy-source pattern
pr-target-validator.yml already uses. The PR can change its title but not
the ruler that measures it. An existsSync bootstrap guard skips the check
when the matcher isn't on the base branch yet (the introducing PR); every
PR after merge is fully gated. This keeps the single shared matcher (#1549's
whole point) rather than forking the regex into the workflow.
Also per review:
- add tests/conventional-title.property.test.cjs (fast-check): any
`type(#n): summary` round-trips to valid; evaluatePrTitle/classifyBucket
are total functions (never throw).
- pin the `fix(#):` zero-digit boundary as missing-issue-ref.
Claude-Session: https://claude.ai/code/session_01VqUHNQCh71pEqjo96zkgQL
---------
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
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
* feat(#441): add /gsd-capture --list-seeds for seed listing and audit
Seeds (.planning/seeds/SEED-NNN-slug.md) could only be created (--seed),
enriched (--enrich), or auto-surfaced at /gsd-new-milestone. There was no way
to browse or audit parked seeds on demand. This adds a read-only listing,
following the established --list → workflow pattern (per the approved scope on
- gsd-tools `list-seeds [status]` (cmdListSeeds in src/commands.cts): scans the
seeds dir, returns { count, seeds[], summary } JSON with each seed's id,
slug, status, scope, trigger_when, planted, title. Optional case-insensitive
status filter. User-controlled content is sanitized (sanitizeForDisplay) and
every path validated (requireSafePath); read-only. Independent of
audit.scanSeeds, which only returns unimplemented seeds for the milestone surface.
- /gsd-capture --list-seeds routes to a new read-only list-seeds workflow that
renders the seed table.
Closes#441
* chore(#441): point changeset fragment at PR #722
* test(#441): allowlist list-seeds test in prompt-injection scan
The test asserts that list-seeds neutralizes injection payloads
(<system>, [INST]) embedded in seed content, so the fixtures legitimately
contain those patterns — same as the sibling security tests already on the
allowlist.
* fix(#441): use canonical /gsd:capture colon form in list-seeds workflow
Claude-facing source (commands/, agents/, gsd-core/workflows/, ...) must use
the /gsd:<cmd> colon form per ADR/CONTEXT.md; the hyphen /gsd-<cmd> form is
retired there (enforced by bug-2543-gsd-slash-namespace.test.cjs). The new
list-seeds workflow used the hyphen form.
* docs(#441): sync help full.md + INVENTORY for --list-seeds
Adds the --list-seeds entry to the help reference (help/modes/full.md, per
bug-2954 argument-hint↔help parity) and registers the new list-seeds workflow
in docs/INVENTORY.md (88→89) and the generated INVENTORY-MANIFEST.json.
* docs(#441): add --list-seeds how-to + drop phantom statuses
Addresses CHANGES_REQUESTED on PR #722 (two documentation blockers):
- USER-GUIDE.md Seeds section (how-to): extend the task to cover
auditing parked seeds on demand via --list-seeds, including the
status filter — kept task-oriented per Diataxis how-to mode.
- CLI-TOOLS.md (reference): drop phantom statuses implemented|rejected
from the list-seeds filter vocabulary; the system only produces
dormant|active|triggered (src/audit.cts scanSeeds). Reference must
be factually accurate and complete.
* fix(#441): guard non-scalar status frontmatter in cmdListSeeds
A seed with a bare `status:` line (extractFrontmatter yields {}) or a
`status: [a, b]` value (yields an array) crashed the whole audit list:
`(fm.status || 'dormant').toLowerCase()` throws a TypeError on a non-string.
Coerce every frontmatter read through a `fmStr` helper (mirrors the existing
`typeof fm.id === 'string'` guard), so a non-scalar status falls back to
dormant and non-scalar scope/trigger_when/title can no longer leak a raw
array/object into the JSON contract. Title is now capped symmetrically.
Adds regression coverage for empty and array `status:` and non-scalar fields.
Refs #441
* docs(#441): align list-seeds workflow status vocabulary
The load_seeds step listed `implemented` as an example status filter, but the
real seed vocabulary is dormant|active|triggered (src/audit.cts scanSeeds);
`implemented` has no producer. Matches the earlier CLI-TOOLS.md correction.
Refs #441
* refactor(#441): extract pure deriveSeedIdentity; match raw status in list-seeds
Pull the seed_id/slug derivation out of cmdListSeeds into a pure, exported
deriveSeedIdentity(stem, rawFmId) so the parsing contract can be property-tested
in-process (review minor #1). No behavior change.
Filter comparison now matches the raw lowercased status (both sides already
normalized) instead of sanitizeForDisplay(status); sanitization is for output,
not matching (review nit #3).
* test(#441): add fast-check property coverage and count=1 boundary for list-seeds
Adds tests/list-seeds.property.test.cjs with four fast-check properties over
deriveSeedIdentity (never-throws, string-only contract, canonical id->seed_id/slug
invariant, filename-prefix fallback) per RULESET.TESTS.property-based-testing
(review minor #1).
Adds an N==1 status-filter boundary case to list-seeds.test.cjs (review minor #2).
* chore(#441): sync runtime launcher snippet into list-seeds workflow
Propagate the current _runtime-launcher.snippet.sh (with non-Claude
runtime home probes) into the new list-seeds.md workflow via
scripts/sync-runtime-launcher.cjs, satisfying bug-891 (E) propagation.
* test(#441): record list-seeds.md in workflow size baseline (#1074)
---------
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
* test(#1464): add behavioral manifest-validation test for capability tutorial docs
Extracts JSON capability manifests from tutorial/reference docs and validates
them through the real validateCapability — closing the test gap that let
issue #1464's broken tutorial manifest (step missing ref) pass undetected.
Adds fix-1464-docs-manifest-validation.test.cjs with:
- Suite 1: build/install/reference docs' complete manifests pass validateCapability
- Suite 2: adversarial fixtures prove the original #1464 bug shapes are caught
(step without ref → "steps[0].ref must be an object…"; id/folderId mismatch)
- Suite 3: extractManifests helper unit tests (complete vs. partial block filtering)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#1496): allowlist docs module for 3-file test cluster
docs-parity-live-registry, docs-update, and the new
fix-1464-docs-manifest-validation sit on the same docs production
module; add the docs entry to lint-test-file-count.allowlist.json
so the novel-offender CI gate passes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1437): add phase.list-plans to gsd-tools
Register phase.list-plans in PHASE_COMMAND_ALIASES, implement
cmdPhaseListPlans in src/phase.cts (uses findPhaseInternal + scanPhasePlans
to return plan_count/has_plans/plans/phase_dir), and wire the handler in
phase-command-router. Previously every call produced "Unknown phase
subcommand".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1437): register new test file in lint-test-file-count allowlist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1437): rename test to fix-NNN convention; update file-count allowlist
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#1435): capability matrix (generated + drift-guarded) + trust-model doc consolidation
ADR-1244 Phase 6. Adds the capability matrix reference, generated FROM the committed registry so it
can never drift from the actual capability set:
- scripts/gen-capability-matrix.cjs (--write / --check); --check is a CI drift guard.
- tests/capability-matrix-sync.test.cjs (4 tests): drift guard, buildMatrix==committed, every cap
present, no placeholders.
- docs/reference/capability-matrix.md regenerated from the registry (release-stable: shows engines.gsd,
omits the lockstep per-cap version that would churn the file every release).
- Consolidated the duplicate trust-model doc: deleted docs/explanation/the-capability-trust-model.md,
merged its content into capability-trust-model.md, redirected ~10 references; no stale links remain.
- Diataxis verification (now that gsd capability is a real command): corrected the matrix's third-party
section — the matrix is the first-party catalogue; the overlay-aware view of installed third-party
capabilities is 'gsd capability list', not this generated file.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1435): Added changeset for the capability matrix reference
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1435): address code-review — non-vacuous matrix test + generator polish
- capability-matrix-sync.test.cjs: assert the 'security registers a ship:pre gate' precondition
unconditionally so the extension-point check can never degrade to a vacuous pass on registry drift.
- gen-capability-matrix.cjs: warn (stderr) on an unknown loop point at generation time; rename
enginesOf -> fmtEngines for consistency with the other fmt* helpers (output unchanged).
- capability-trust-model.md: point the two how-to links at the real files
(import-a-capability-from-a-url.md, version-a-capability.md) instead of the bare directory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1435): backfill changeset PR number → #1458
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1431): runtime capability registry overlay (ADR-1244 Phase 2)
Promote the registry from a frozen data file to loadRegistry({includeInstalled}),
composing the first-party registry with a validated installed overlay (ADR-1244 D2):
- Extract the conformance validator to a shared runtime-callable module
(gsd-core/bin/lib/capability-validator.cjs); the generator re-exports it
verbatim, guarded by a generative-parity test (no build-time/runtime drift).
- capability-loader.cts: loadRegistry({includeInstalled}) composes first-party
∪ validated overlay from $GSD_HOME/.gsd/capabilities (global) and
<root>/.gsd/capabilities (project) via the canonical buildRegistry. First-party
always wins (id/skill/agent/config/command-family + reserved gsd-/anthropic-
prefixes); full merged-set cross-capability validation; engines.gsd load-time
re-gate (skip-with-warning); gate-kind capabilities FAIL CLOSED; fragment-path
escapes rejected.
- semverSatisfies (hand-written, no dep) for the engines.gsd gate, fail-closed.
- Wire surface/state + loop to the overlay; loop injects a blocking gate for each
skipped gate-kind overlay (fail-closed).
- cwd-aware overlay config-key federation: config-loader _federatedConfigSchema(cwd)
+ config-schema isValidConfigKey(key, cwd) compose the overlay per loadConfig/
config-set call (never eager at module load, never wrong-cwd); first-party path
unchanged with no cwd.
- run-tests.cjs sandboxes GSD_HOME (idempotent — nested spawns reuse it) for test
hermeticity; capability-loader.cjs git+eslint-ignored (tsc artifact);
capability-validator.cjs stays linted (#551 migration coverage).
Closes#1431
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1431): add changeset for runtime capability registry overlay
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#1431): kill config-schema cwd-aware federation mutants (Stryker ≥52)
The cwd-aware overlay config-key federation added to config-schema.cts
(_capabilityConfigSchema(cwd) + isCapabilityConfigKey/isValidConfigKey cwd
threading) introduced mutable surface uncovered by config-schema's mutation
test set, dropping its score to 39.58% (below the 52 break threshold). Add a
real-overlay-fixture describe block exercising every branch (cwd guard, overlay
loadRegistry, found-branch, first-party fallback, cwd threading); local Stryker
score 39.58% -> 77.08%.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1430): versioned capability manifest + native stamping (ADR-1244 Phase 1)
Make the capability manifest versioned — the data substrate the Capability
Ecosystem (ADR-1244) keys off:
- capability.json gains a REQUIRED semver `version` plus the optional
ecosystem envelope (`engines.gsd`, `compatVersions`, `integrity`,
`provenance`); the build-time conformance validator enforces them via a new
`validateVersionEnvelope()` (exported for the Phase 2 runtime overlay).
- All 32 native capabilities stamped with `version` (= package version,
lockstep) + `engines.gsd`; `sync-manifest-versions.cjs` gains a glob sweep
that keeps them in sync, and the issue-844 regression guard is extended.
- Strict SemVer 2.0.0 grammar blocks metacharacter/space/unicode smuggling in
version strings; range/integrity fields are shape-validated (satisfaction
and the load-time gate are deferred to Phase 2/4).
- Capability rel-paths emitted forward-slash for cross-platform git correctness.
Closes#1430
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1430): add changeset for versioned capability manifest
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds scripts/lint-resolution-provenance.cjs — a registry + ratchet CI guard
that locks in the agent-skills configured_empty/not_configured contract tests
so they cannot be silently removed, and establishes a registration point for
future config-interpreting read verbs (ADR-1411 P4).
Design rationale:
- REGISTRY (one entry: agent-skills → src/init.cts → tests/agent-skills.test.cjs)
is the canonical registration site; new verbs are added here.
- For each registered verb, the guard asserts its test file contains BOTH a
`configured_empty` assertion AND a `not_configured` assertion — proving the
configured-empty-vs-not-configured contract is explicitly tested.
- NOT a universal static detector (intractable / false positives) — mirrors the
no-adhoc-markdown-parsing grandfather pattern.
- Uses scripts/lib/allowlist-ratchet.cjs (assertWithinAllowlist) so stale
allowlist entries fail (ratchet-down) and novel offenders always fail.
- checkRegistry() is factored as a pure exported function tested in
tests/lint-resolution-provenance.test.cjs without shelling out.
- Wired into lint:ci (package.json) and lint step name updated in test.yml.
- CONTEXT.md ### Resolution Convention extended with P4 guard sentence.
- Allowlist starts empty ([]) — agent-skills already has its tests.
Closes#1417
Part of #1411
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1355): detect-and-warn guard for claude-code agent-teams
GSD's multi-agent orchestration can stall under claude-code's experimental
agent-teams (a subagent's completion fails to route to the orchestrator). Per
the maintainer decision, the accepted scope is a read-only detector + one
non-fatal warning — NOT the declined run_in_background/TaskOutput conversion.
- New Teams Status Module (src/teams-status.cts → gsd-core/bin/lib/teams-status.cjs):
pure resolveTeamsStatus({runtime, env}) + thin CLI cmdTeamsStatus reusing
resolveRuntime. active = strictly-truthy env flag AND runtime === 'claude'.
- Wire `gsd-tools query teams-status [--active]` (read-only; no capability
registration needed — conformance gates govern features, not query commands).
- One non-fatal warning in plan-phase.md before the first Agent spawn, gated on
`query teams-status --active`; zero behavior change on non-claude/teams-off.
- Hermeticity: clear CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS in run-tests.cjs +
SESSION_ENV_KEYS. Docs reference + CONTEXT.md glossary. Built lib gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1355): add changeset for teams-detect guard
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#1355): bump plan-phase.md workflow size baseline (+407B for teams warning)
The non-fatal agent-teams warning block added to plan-phase.md grew it
92759 → 93166 bytes, past its committed per-file baseline ratchet. The growth
is small, deliberate, and still well under the workflow tier hard cap. Regenerate
the baseline via `npm run size:baseline` (only plan-phase.md changed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1355): register teams-status.cjs in the inventory manifest
The new teams-status CLI module is a tracked surface; regenerate
docs/INVENTORY-MANIFEST.json (cli_modules family) via
gen-inventory-manifest.cjs --write so the inventory-manifest-sync gate passes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ci-prepare-test-scope.cjs's empty-detection FALLBACK hardcoded
tests/core.test.cjs, deleted in #1291. Every scoped lane (scope=targeted|
windows) that hit the fallback wrote the stale path into .ci-selected-tests.txt
and crashed run-tests with "requested test file(s) not found: core.test.cjs".
The full/sharded lanes glob the suite and were immune, so only the scoped
lanes went red (e.g. run 27599149212 on #1308).
Existence-filter the FALLBACK at write time and fall back to the 'unit' suite
sentinel (the #408/#641 path, resolved live by run-tests) when nothing
survives, so a stale reference degrades instead of crashing the lane. Detected
lists still pass through verbatim (they may carry a suite sentinel and are
already filtered by affected-tests-lib). Refactor to an exported, testable
resolveSelection().
Add a generative parity guard (DEFECT.GENERATIVE-FIX) asserting every FALLBACK
entry resolves on disk or is a known suite sentinel — it fails the instant a
refactor deletes a listed file, which #1291 did and CI did not catch — plus
resolveSelection unit tests and an end-to-end subprocess test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vitest.config.ts configured Vitest (not a dependency) to run .ts test files (the repo has none) rooted at ./sdk, a directory deleted when the SDK package seam was retired in #191 (ADR-0174). No npm script, workflow, or dependency references it.
Also drop the now-dead sdk/src/*.test.* branch in diff-touches-shipped-paths.cjs isCiGating(), which can never match since the sdk/ tree no longer exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an optional activationKey to the feature role of capability.json — the
dotted config key that gates the whole capability (e.g. graphify.enabled).
gen-capability-registry validates it (non-empty string, reserved-name guard,
must be declared in the capability's own config slice, feature-only) and emits
it per-capability in the generated registry. Declared on graphify + intel.
No runtime consumption yet (resolver wiring lands in #1305). Part of #1302.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(#1294): T-final — delete the core.cjs re-export spine (epic #1267 complete)
After T0–T6 nothing imports core, so retire the spine and its scaffolding:
- delete src/core.cts (and the gitignored gsd-core/bin/lib/core.cjs artifact;
remove its .gitignore + eslint-ignore entries)
- delete scripts/lint-core-spine-imports.cjs + its allowlist; drop it from the
package.json lint:ci chain
- regenerate docs/INVENTORY-MANIFEST.json (drops the core.cjs surface)
- sweep stale references: CONTEXT.md glossary back-compat clauses (spine retired,
callers import the leaf directly), planning-config.md CONFIG_DEFAULTS owner,
and false present-tense core.cjs claims in leaf-module docstrings
The ADR-857 decomposition is complete: the former Core god-module is fully
dissolved into its leaf modules; no re-export spine remains. No behaviour change.
Closes#1294
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1294): migrate the computed-path core.cjs importers the literal grep missed
bin/install.js used require(path.join(_gsdLibDir, 'core.cjs')) (a computed
path, and bin/install.js was never in the convergence lint's scan roots), and
~8 test files referenced core.cjs via path.join/readFileSync/existsSync/FILE_ARG
forms the literal-string migration grep missed. Route install.js's symbols to
their leaves (RUNTIME_PROFILE_MAP->model-catalog, resolveTierEntry/EFFORT_SET->
model-resolver) and repoint/adjust the test references to the leaves. Recovers
the 161 'Cannot find module core.cjs' failures from the spine deletion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The convergence lint only scanned src/ + gsd-core/bin, so ~35 test files
still imported core.cjs. Repoint all 33 behaviour importers to the leaf
modules directly (same symbol->leaf map as the src migration; leaves are the
objects core re-exported by reference), delete the now-meaningless
shim-identity describe blocks in the 8 leaf tests, and delete tests/core.test.cjs
(forwarded-behaviour coverage now lives at the leaves; resolveWorktreeRoot
test relocated to worktree-safety in T0) and tests/lint-core-spine-imports.test.cjs
(the lint is removed in T-final). Dropped the stale core.test.cjs entries from
the allow-test-rule-refs allowlist; eslint-rules RuleTester fixture path
pointed at io.cjs.
After T6: ZERO test imports core.cjs. core.cts still builds (now fully unused);
T-final deletes it. No behaviour change.
Closes#1291
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Adds mcp__perplexity__* to both researcher profiles (generated source-of-truth) and regenerates the agents; adds a generative dispatch-table↔tools parity guard so future provider drift fails CI. Regenerates the agent-size baseline for the +20-byte frontmatter growth.
Fixes#1284
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate the entire core surface of commands (~23 symbols), phase (~17),
roadmap, state, template to the leaf modules directly (behaviour-identical —
leaves are the objects core re-exports by reference). All 5 now import zero
core symbols and are removed from the allowlist (9 -> 4). Dropped a dead
`void replaceInCurrentMilestone` from phase.cts; stale core.cjs docstrings
fixed. core.cts re-exports untouched (serve the remaining 4 idiom-hard
files); teardown is T-final. No behaviour change.
Closes#1286
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Migrate 9 files' entire core surface to the leaf modules directly
(behaviour-identical — leaves are the objects core re-exports by reference):
config, docs, gap-checker, graphify-command-router (namespace core.output ->
io.output), init (17 core symbols -> 8 leaves), profile-output, uat,
verification, workstream.
All 9 now import zero core symbols and are removed from the allowlist
(18 -> 9). Stale core.* docstrings corrected. core.cts re-exports untouched
(still serve the remaining 9 files); teardown is T-final. No behaviour change.
Closes#1283
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Per the T1 design rubber-duck, batch by FILE so each tranche drops
convergence-lint allowlist entries. Migrate 12 files' core imports to the
leaf modules directly (behaviour-identical — leaves are the objects core
re-exports by reference):
- io (output/error/ERROR_REASON): agent-command-router, capability-state,
capability-writer, frontmatter, gsd2-import, learnings, loop-resolver,
task-command-router
- roadmap-command-router -> config-loader; workstream-inventory -> core-utils
- milestone, verify -> their full leaf sets (both were multi-leaf, not
single-leaf as first scoped; migrated completely)
All 12 files now import zero core symbols and are removed from the
allowlist (30 -> 18). core.cts re-exports untouched (still serve the
remaining 18 files); teardown is T-final. Stale core.cjs docstrings in the
migrated files corrected to reference io.cjs. No behaviour change.
Closes#1281
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Re-home the 6 implementation functions squatting in the core.cjs re-export
spine (ADR-857) into the modules whose interface they belong to, with core
re-exporting them BY REFERENCE so all 32 callers + the shim-identity tests
keep resolving unchanged:
- worktree-safety: resolveWorktreeRoot, pruneOrphanedWorktrees
- git-base-branch (broadened to the Git Query Module): gitWorktreeInfoInternal
- agent-install-check (new leaf): getAgentsDir, checkAgentsInstalled
- delete the _resetRuntimeWarningCacheForTests wrapper; consumers use a
shared resetRuntimeWarningCaches() helper in tests/helpers.cjs
Add scripts/lint-core-spine-imports.cjs (migration-convergence lint with a
30-importer allowlist, wired into lint:ci) so the staged spine retirement
provably converges: CI fails on any new ./core import. Register the new
generated agent-install-check.cjs in eslint-ignore + .gitignore +
INVENTORY-MANIFEST.json.
No behaviour change. First tranche (T0) of epic #1267.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1243): consume Claude plugin-provided skills via native Skill-tool directive + grant Skill to agent_skills-consumer agents
- Relax global skill name validation to accept namespaced form `^[A-Za-z0-9_-]+(:[A-Za-z0-9_-]+)*$`
- Namespaced names (containing colon) on claude runtime emit a Skill-tool load directive instead of a @-include line
- Namespaced names on non-claude runtimes are skipped with a warning
- Bare unresolved names retain existing warn-and-skip behavior (no promotion to directive)
- Grant `Skill` tool to all 22 agent_skills consumer agents; 5 generated agents updated via research-profiles.cjs + regen, 17 hand-authored agents edited directly
- Add 16 TDD tests in describe('bug #1243') covering happy/mixed/precedence/negative/cross-runtime/regression/grant cases
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(#1243): document plugin-provided skills in agent_skills
Update the Agent Skills Injection reference in CONFIGURATION.md with
the three entry forms (project-relative, global:<name>,
global:<plugin>:<skill>), the Claude-only runtime behaviour of the
namespaced form and the warn-skip on other runtimes, the plugin
pre-install prerequisite, and the consumer-agent Skill tool grant.
Add docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md with a
step-by-step guide for installing the plugin, locating the namespaced
skill name, wiring it into agent_skills, and verifying injection.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#1243): align agent_skills docs with emitted block format + mixed-block regression test (code-review)
- Replace two-section mixed-block example (bogus "Load these plugin-provided skills using the Skill tool:" header) with the actual single-section inline format in CONFIGURATION.md and docs/how-to/attach-a-plugin-skill-to-a-gsd-agent.md
- Fix quoted warning text in how-to doc to exactly match the emitted string: [agent-skills] WARNING: Plugin-namespaced skill "global:<name>" requires a Skill-tool-capable runtime (claude) — skipping on runtime "<runtime>"
- Replace phantom agent slugs (gsd-checker, gsd-researcher, gsd-advisor, gsd-synthesizer) in CONFIGURATION.md Supported Agent Types with real agents/gsd-*.md examples (gsd-plan-checker, gsd-phase-researcher, gsd-code-reviewer, gsd-ui-auditor, gsd-research-synthesizer)
- Add byte-identical mixed-block regression test: one path-resolvable global skill + one plugin-namespaced skill on claude runtime → asserts r.ir.block === single-section interleaved block, no secondary header
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(#1243): regenerate agent-size baseline for the Skill-tool grant
The 22 agent_skills-consumer agents each grew +7 bytes from adding `Skill`
to their tools list; refresh the committed per-agent size baseline (#1074 guard).
* chore(#1243): add Added changeset fragment
* fix(#1243): traceable allow-test-rule ref + separator-agnostic byte-identical tests (CI)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(#1191): inject clock/reset testability seams + handle valid-null settings
- worktree-safety reapOrphanWorktrees: injectable deps.nowMs clock for deterministic stale-lock boundary tests (mirrors snapshotWorktreeInventory's options.nowMs).
- active-workstream-store: _resetControllingTtyCacheForTests() seam clears the memoized controlling-TTY probe cache; test replaces require.cache busting.
- gen-capability-registry: export stripGeneratedComment (additive); test imports the real helper + equivalence assertion, keeping the deliberate drift oracle.
- install.js readSettings: a successfully-parsed JSON null is treated as empty settings ({}) instead of being mis-reported as malformed; genuine parse failures still warn. readSettings/stripJsonComments exported (GSD_TEST_MODE-guarded require) for real behavioral tests.
Closes#1191
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1191): add changeset for valid-null settings fix (#1233)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1191): replace Stryker-incompatible structural reset test with behavioral isTTY-spy
The seam-2 reset test read the BUILT active-workstream-store.cjs and grepped for 'didProbeControllingTtyToken = false' — Stryker instruments that file so the literal is absent, failing the mutation DRY RUN. Replaced with a behavioral test that spies on process.stdin.isTTY access count to prove a post-reset probe re-runs (kills the didProbe-reset mutant) without reading source text. Local stryker: dry run passes, score 85.21% >= 80.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ADR-230's branching-model gate (pr-target-validator.yml) decided allowed/blocked PR targets via inline regex in github-script — untestable. Extracted the decision into committed scripts/pr-target-policy.cjs (classifyPrTarget(base,head)->{decision}), and rewired the workflow to checkout the BASE ref (trusted; fork-tamper-safe) + require the module. Behavior-identical (Codex-verified char-by-char regex equivalence + all side-effects preserved). 70 tests incl. an equivalence oracle battery + hyphen-boundary negatives. Added contents:read for the checkout. Re-attribution: no ADR-230 test references exist (issue's '2 misattributed files' claim not borne out).
Closes#1190
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1223): install scripts/fix-slash-commands.cjs so gsd-tools loads
Before this fix, bin/install.js copied scripts/changeset/ and scripts/lib/
into the runtime config dir but omitted scripts/fix-slash-commands.cjs.
gsd-core/bin/lib/command-roster.cjs requires this file at module load via
require('../../../scripts/fix-slash-commands.cjs'), so every gsd-tools
command crashed with MODULE_NOT_FOUND on every installed runtime.
Four changes:
- bin/install.js copy step: copy fix-slash-commands.cjs into <configDir>/scripts/
with source-missing hard-fail and verifyFileInstalled smoke check
- bin/install.js writeManifest: track scripts/fix-slash-commands.cjs (not
covered by the changeset/lib subdir loops)
- bin/install.js uninstall: best-effort unlinkSync before scripts/ rmdir
- scripts/fix-slash-commands.cjs readCmdNames(): wrap readdirSync in
try/catch returning [] so skill-based/global installs without a local
commands/gsd/ directory do not throw ENOENT
Tests added to tests/install.test.cjs (6 new tests):
- smoke: install() copies fix-slash-commands.cjs
- e2e: spawned gsd-tools.cjs does not crash with MODULE_NOT_FOUND
- manifest: writeManifest() tracks the file
- uninstall: uninstall() removes the file
- readCmdNames unit: export returns an array
- readCmdNames spawn: absent COMMANDS_DIR returns exit 0 (no throw)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1223): backfill changeset PR number (#1240)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1224): accept --pr 0 placeholder at changeset creation
The required-field guard `!opts.pr` treated the integer 0 as falsy,
rejecting the documented `pr: 0` two-push placeholder with a usage
error (exit 2). Non-numeric `--pr abc` (NaN) was also silently
accepted before (passes `!NaN === true`... actually `!NaN` is true, so
NaN would trigger the guard already). The new explicit checks use
`opts.pr === null` for missing flag and `Number.isNaN` for non-numeric
input, accepting all finite integer values including 0.
The merge-time safety net in parse.cjs (`pr <= 0` → INVALID_PR) is
unchanged — a pr:0 fragment is still rejected at lint/render time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1224): backfill changeset PR number (#1231)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Extends `dispatchKindEntry` in `runtime-artifact-layout.cts` to route
agents-kind entries through a converter when the descriptor carries a
non-null `converter` field. Adds `stageAgentsForRuntimeWithConverter`
to `install-profiles.cts`, expands `VALID_CONVERTER_NAMES` with the 9
agent converter names, and adds a fail-first behavioral test suite
(9 tests) proving the new wiring end-to-end.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `full test (windows-latest, *)` lane ran the entire unit suite (~740+
files) in one job whose wall-clock crept against the 20m cap and intermittently
CANCELLED (false-negative gate, observed on PR #1207). Prior tactical fixes
#869 (15→20m bump) and #1051 (handle-leak) deferred the cliff structurally.
Shard the unit suite across 3 parallel runners per OS/node leg so per-job
wall-clock is O(total/3) and stays under the cap as the suite grows.
- scripts/run-tests.cjs: add `--shard <i>/<n>` — a deterministic, balanced
round-robin partition (fileIndex % n === i-1) over the SORTED selected file
list. parseShardArg strictly validates i∈1..n, n≥1, integer-only; n=1 is a
pure no-op. The 28K Windows argv chunking is preserved within each shard. A
legitimately-empty shard (n > file count) exits 0; a selection empty BEFORE
sharding still hits the discovery hard error. Composes with --suite and is
order-independent (sorted before partition). Exports selectShard/parseShardArg.
- .github/workflows/test.yml: test-full becomes the 3 legs × 3 shards = 9-job
cross-product (explicit include rows — a base shard dim does not cross-product
with include legs, and a nested matrix.leg.os is unresolvable by the H1
shell-policy linter). Unit suite runs sharded; integration/security run once
per leg (shard 1). The Required tests fan-in is unchanged: it already needs
test-full and checks the matrix-aggregate result, so a failed/cancelled shard
fails the gate; the branch-protection check name is preserved.
- tests: partition/CLI + pure selectShard contract (completeness, disjointness,
balance, determinism, boundaries, fast-check property) + parseShardArg
validation, in run-tests-harness.test.cjs; a DEFECT.GENERATIVE-FIX parity
guard (per-row shard values 1..N, every leg runs all shards, N == --shard /N
denominator) + Required-tests name/needs pin, in ci-test-scope.test.cjs.
Closes#1212
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Core half of #1105: a legal external_job_waiting deferred state so an async-dispatched Execute step (committing a .planning/async-jobs/<job>.json manifest, deferring SUMMARY.md) is not an illegal partial. execute-phase safe-resume, resume-project, and pause-work reconcile against the versioned scheduler-agnostic manifest stability contract without re-dispatching; the producer is the capability half (#1164). Closes#1165.
* fix(#1205): roadmapper applies phase_id_convention to generated phase IDs
- Add Phase ID Convention section to <phase_identification> block:
documents sequential (default) vs milestone-prefixed forms, and
instructs the agent to read phase_id_convention from config.json
- Update <output_formats> to show both header and checklist forms for
sequential and milestone-prefixed conventions with examples
(e.g. ### Phase 1-01: Name, - [ ] **Phase 1-01: Name**)
- Add TDD regression test tests/bug-1205-roadmapper-convention.test.cjs
(5 assertions, confirmed fail-first then pass after fix)
- Update tests/agent-size-baseline.json to reflect legitimate growth
- Add .changeset/brave-otters-leap.md (Fixed, pr:0 placeholder)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: backfill changeset pr: 1215 for fix/1205
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(#1205): move phase_id_convention regression into roadmapper-granularity.test.cjs
lint-regression-test-names rejects new standalone bug-NNNN-*.test.cjs files;
regression cases must live in the owning module's test file.
Move the 5 phase_id_convention assertions (#1205 regression) from the
removed tests/bug-1205-roadmapper-convention.test.cjs into
tests/roadmapper-granularity.test.cjs as a new describe block, alongside
the existing granularity calibration tests. Also update the allow-test-rule
comment to cover both #163 and #1205 surface contracts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(#1205): fix lint-allow-test-rule-refs for roadmapper-granularity
- Add issue ref (see #1205) to allow-test-rule comment in
tests/roadmapper-granularity.test.cjs so lint-allow-test-rule-refs
passes (new exemptions require #NNN per ADR-456)
- Prune stale 'source-text-is-the-product' entry from
scripts/lint-allow-test-rule-refs.allowlist.json (ratchet-down;
comment now compliant and no longer needs grandfathering)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#997): ensure canonical ~/.claude/gsd-core path for plugin installs via SessionStart hook
Claude Code marketplace plugin installs unpack the package into the
version-pinned plugin cache and never run bin/install.js, so
~/.claude/gsd-core/ is never created. Agents, commands, and templates
markdown-@-include the canonical ~/.claude/gsd-core/... path (which
expands ~ but NOT ${CLAUDE_PLUGIN_ROOT}), so every include resolved to
nothing and agents (e.g. the executor) failed.
Add a SessionStart hook (hooks/gsd-ensure-canonical-path.js) that, on a
plugin install, symlinks the canonical path's immutable subdirs (bin,
contexts, references, templates, workflows) to the plugin's bundled
gsd-core/ tree. It changes zero @-references, is a no-op in classic
installs, preserves user-generated files (USER-PROFILE.md, STATE.md),
prunes stale links so it self-heals after `claude plugin update`, uses
Windows junctions, and rejects bundled/canonical paths that escape the
resolved plugin root (no traversal, no clobber).
Registered in HOOKS_TO_COPY (build-hooks), MANAGED_HOOKS, hooks.json
SessionStart (runs first, timeout 5), and BUNDLED_GSD_HOOK_FILES.
Behavioral regression tests folded into issue-766-plugin-manifest.test.cjs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(#997): backfill changeset PR number to #1207
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(#1187): per-module mutation-score ratchet + graduate core-utils
ADR-456's 80% mutation floor was unenforceable as a single global break=50:
4 of 6 covered modules sit at 63-79% and forcing them to 80 would require
brittle exact-string assertions on equivalent string-literal mutants (a
Goodhart's-Law trap). Instead, each covered module declares a minScore floor
(locked at its measured score, TARGET 80) enforced per CI shard via
stryker --break, ratcheting up over time without brittle tests.
- mutation-matrix.cjs: minScore per module + TARGET_MUTATION_SCORE=80, emitted
in the matrix; require.main guard + exports for testability.
- mutation.yml: per-shard --break <minScore>.
- stryker.config.mjs: global break 50->60 as a local backstop (CI uses minScore).
- Graduated core-utils (measured 77.5%, floor 75).
- context-utilization 79.5->92.3% via behavioral killers (state classification
outputs + error-value contract, not exact-string matches) -> minScore 80 (TARGET).
- ratchet-integrity guard test (28 cases).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1187): pass mutation break via MUTATION_BREAK env (no stryker --break flag)
Adversarial review caught that Stryker 9.x has no --break CLI flag, so the
per-shard 'stryker run --break <minScore>' errored out every mutation shard.
Read the per-module floor from process.env.MUTATION_BREAK in stryker.config.mjs
and set it per shard via env in mutation.yml. Red-green verified: MUTATION_BREAK=99
exits 1, =80 exits 0. Also make the ratchet guard monotonic (RATCHET_BASELINE
floors; lowering a floor now fails the guard unless the baseline is edited).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1187): fail closed on bad MUTATION_BREAK + monotonic ratchet baseline
Code review: Number(env)||60 failed OPEN — an empty/invalid MUTATION_BREAK
(e.g. a future module missing minScore -> matrix expands to '') silently
degraded the shard to break 60, letting a high-floor module regress undetected.
resolveMutationBreak() now returns 60 only when the env is truly unset (local
backstop) and THROWS on present-but-empty/non-numeric/out-of-range (fail closed);
stryker.config.mjs imports it via createRequire. Also make RATCHET_BASELINE an
equality mirror (=== not >=) so any floor change is explicit in review and no
floor can be silently lowered. Tests: 46 (incl resolveMutationBreak cases).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1187): recalibrate config-schema/prompt-budget floors to CI scores
First CI mutation run failed two shards: the floors were set from local Stryker
runs whose TIMEOUTS were counted as kills (env-variable), inflating scores. CI
runs with timeout~0, so the real deterministic scores are lower:
- config-schema: local 69.7% -> CI 54.55% (5 local timeouts vanished) -> floor 52
- prompt-budget: local 99.6% -> CI 68.33% (239 local timeouts vanished) -> floor 66
Calibrate floors from CI (the documented source of truth) and record the lesson
in the comment so future floors aren't set from timeout-inflated local runs.
Baseline updated to match. The other 5 shards passed (deterministic CI scores
above their floors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1146): single base-branch resolver across forking workflows
Replaces duplicated per-workflow bash detection that silently fell through
to :-main on repos where origin/HEAD is unset (git init+remote add+fetch
without set-head, most CI checkouts, many worktrees).
New CJS module git-base-branch.cjs exposes `gsd_run query git.base-branch`
with full precedence ladder: git.base_branch config override → origin/HEAD
symref → git remote show origin (authoritative) → local branch presence →
"main". All git subprocesses bounded with timeouts; degrades gracefully.
Wires execute-phase, quick, ship, complete-milestone, and pr-branch to the
single resolver. Removes 14 lines of duplicated detection bash across the
five workflows.
Includes 7 behavioral tests covering the full precedence ladder including the
key regression case (master repo, origin/HEAD unset → must return "master",
NOT "main") and an anti-regression guard that fails if any workflow
re-introduces the :-main/:-master fallback pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(changeset): backfill PR number #1198
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(#1146): drop stray PR-body file from branch
pr-1146-body.md was committed during changeset backfill but must not
be tracked in the repo. Content preserved externally for PR body use.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#1146): add tests for flat base_branch config key and both-branch tie-break
Closes two mutation gaps identified in adversarial review:
- A2: flat {base_branch: ...} at config root (legacy key form) was covered
by code but unguarded against mutation of lines 74-75 in resolver
- H: tier-4 tie-break when both main+master exist locally (main wins,
per tryLocalBranch JSDoc) was documented but untested
9/9 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#1146): allowlist workflow-literal guard as runtime-contract exemption
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#1146): degrade gracefully when gsd_run unavailable in handle_branching bash blocks
handle_branching (execute-phase.md) and step 2.5 (quick.md) are extracted
and run verbatim by behavioral tests that lack the gsd_run preamble.
Adding a || fallback ladder (git symbolic-ref then echo main) keeps the
unified resolver as primary in real workflows while letting the test harness
succeed without gsd_run defined.
Also propagates updated runtime-launcher preamble to pr-branch.md (added in
origin/next MemPalace PR) and regenerates workflow-size-baseline.json.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>