Fold 48 issue-named gsd-tools CLI regression files into the canonical test file
that owns each subcommand subject (state, roadmap, phase, milestone, audit, config,
router/dispatch, stats, verify, health, etc.), preserving every assertion and its
origin issue number as provenance (block-scoped describe wrappers, 299 subtests
conserved 1:1). No monolithic gsd-tools.test.cjs created — routes into 18 existing
per-subject suites.
Removes 48 tests/ files. Regenerates regression-name allowlist (271->231), ratchets
the file-count allowlist across 6 buckets (audit/milestone/phase/roadmap/state/verify),
and makes 10 relocated allow-test-rule exemptions issue-ref-compliant (ADR-456; prunes
10 stale ids). Repoints one CONTEXT.md symptom ref and ADR-3524's parity-test ref.
lint:ci green.
Part of epic #1969. Closes#1971.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial review (Codex) surfaced a latent cross-suite leak: the folded
bug-130 and bug-410 install.test.cjs blocks set process.env.HOME/USERPROFILE to a
temp home at collection time and never restored — harmless when each ran as its own
process, but after consolidation it leaked into sibling folded suites in the same
process. Scope both mutations to before()/after() hooks that restore the prior
values, matching the save/restore pattern used elsewhere in the file. Assertions
unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* enhance(#1872): document Claude Code advisor inheritance in model profiles
Add an "Advisor Tool (Claude Code)" section to
gsd-core/references/model-profiles.md: session-level advisor is inherited
by all GSD subagents and composes with the per-agent profile/tier system,
candidate executor/advisor pairings per profile (cost/quality/caching
claims attributed to Anthropic's advisor-tool docs, not asserted as GSD
behavior), when it is worth enabling vs not, and the session-level /
no-per-agent-control constraint linking anthropics/claude-code#73072.
Docs-only. Golden-install-parity fixtures recaptured for the edited
reference file (hash-only, one line per runtime).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(#1872): add changeset
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#1872): use Documentation changeset type for docs-only change
Changeset type was `Changed`, which triggers the docs-required lint
(TRIGGERING_TYPES in scripts/lint-docs-required.cjs). This PR only
touches gsd-core/references/model-profiles.md, so there is no docs/
file to pair with and docs-lint failed. `Documentation` is the correct
type for a docs-only enhancement and is exempt from the trigger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(#1872): put docs-exempt marker on its own line, revert to type Changed
The prior fix (type: Documentation) was invalid — parse.cjs ALLOWED_TYPES
is {Added, Changed, Deprecated, Removed, Fixed, Security}, so both
changeset-lint and docs-lint failed with invalid_type.
Real root cause of the original docs-lint failure: DOCS_EXEMPT_RE is
anchored to match the `<!-- docs-exempt: ... -->` marker only on its own
line, but the marker was tacked onto the end of the prose line, so it was
never captured (docsExempt: null) and the triggering `Changed` fragment
had no docs/ pairing -> fail_docs_missing.
Fix: keep the valid `type: Changed` and move the marker to its own line.
Verified locally: changeset-lint -> ok_fragment_present,
docs-lint -> ok (own-line marker parses to ok_fragments_exempt).
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>
* refactor(#1852): lazy-split plan-phase.md into steps/
Extract 3 self-contained, rarely-hit sections into gsd-core/workflows/plan-phase/steps/
via lazy 'Read and execute' pointers (mirrors execute-phase/steps/, ADR-1610 progressive
disclosure — no eager @-import): closed-phase-gate (1.5), prd-express-path (3.5),
windows-troubleshooting. Byte-invariant: plan-phase.md 94459 -> 89775 (-4684), each step
< 32 KiB anchor, resolved instructions unchanged.
Scope note: only 3 sections were extractable. plan-phase.md is guarded by a dense net of
content-presence tests (plan-bounce/enh-3209/phase6-planning-capabilities assert specific
sections/flags inline) that block extracting the larger blocks without also refactoring
those tests — durable low-80s headroom is deferred pending maintainer re-scope (issue #1852).
Cascade: size:baseline regen, 16 golden-install-parity fixtures regen, INVENTORY in sync.
Full plan-phase test surface green (4129/4130, 0 fail); lint:ci exit 0.
* docs(changeset): Changed fragment for #1934 (plan-phase lazy-split)
* docs(changeset): mark #1934 fragment docs-exempt (internal workflow refactor)
* fix(#1852): add gsd_run launcher preamble to prd-express-path step
The extracted prd-express-path.md calls gsd_run but the canonical launcher
preamble lived in the parent plan-phase.md — runtime-launcher-parity (#373)
walks workflows/ recursively and requires every .md using gsd_run to carry
exactly one preamble + the $HOME/.claude fallback arm (same as the existing
execute-phase/steps/ files). Injected via scripts/sync-runtime-launcher.cjs;
golden fixtures + size baseline regenerated. plan-phase.md unchanged (89775).
---------
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
* feat(#1942): VS Code extension — repo-local IDE host with reachability proof
The VS Code extension (vscode/extension.js + vscode/package.json) is a
repo-local, buildable extension (not Marketplace-published). The gsd.invoke
handler dispatches in-process through the GSD command-routing hub. The handler
is exported separately from activate() so it is testable without a VS Code host.
tests/vscode-extension-reachability.test.cjs: proves the handler dispatches
through the hub + returns a result (keystone wired); the manifest declares the
command + engine; resolveEngineRoot finds the gsd-core/ dir.
* docs(changeset): VS Code extension (#1966)
* fix(#1942): register vscode/package.json in VERSIONED_MANIFESTS (#844)
* fix(#1942): align vscode/package.json version with package.json (1.7.0-rc.1)
* fix(#1967): build hooks/dist once upfront in run-tests to close scoped-CI empty-dir race
hooks/dist/ is gitignored and not built by prepare (build:lib only), so the
scoped CI lane starts with it absent. The first install test's before() hook
triggers build-hooks.js, which creates DIST_DIR empty then fills it file-by-
file; a concurrently-spawned install.js reader can observe the empty window and
fail with 'Failed to install hooks: directory is empty' (intermittently failing
e.g. bug-3683-workflow-colon-namespace-leak on scoped legs).
Add ensureBuiltHooks() to scripts/run-tests.cjs — the same upfront chokepoint as
ensureBuiltArtifacts — to build hooks/dist once, single-process, before any
concurrent test spawns install.js. Completeness is checked against
build-hooks.js HOOKS_TO_COPY (absent/empty/partial/zero-byte -> rebuild; complete
-> no-op). Folds regression coverage into bug-969-test-infra-flake-hardening
(Part C), proven fail-first (ensureBuiltHooks undefined on next).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#1967): set changeset pr to 1968
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#1944): pi extension — installable ExtensionAPI host-plugin with reachability proof
The pi extension (pi/gsd.cjs) is a real, jiti-loadable ExtensionAPI module that
registers /gsd (dispatches through the GSD command-routing hub) + gsd_invoke tool
+ tool_call event. Engine entry: in-process CJS require (Bun-compatible). The
extension resolves the engine tree via walk-up (like the OpenCode plugin).
tests/pi-extension-reachability.test.cjs proves the /gsd handler DISPATCHES
through the hub (returns a JSON result) — the keystone-wired proof that the
command is user-reachable, not just registered on a mock (#1944 AC).
* docs(changeset): pi extension (#1965)
* fix(#1944): add docs-exempt marker to changeset (no standalone docs file)
The flattened install layout broke the third-party capability ecosystem in two
ways. Both are fixed at the host-version / installer boundary.
Gap 1 — host version read as 0.0.0. The running GSD version was resolved via
require('../../../package.json') (loader/source) and require('../../package.json')
(the gsd-tools CLI), which in the installed layout is the versionless CommonJS
marker → the fail-closed fallback reported 0.0.0, so `capability install` rejected
any manifest with a real engines.gsd range as "incompatible with GSD 0.0.0". For
runtimes that get no marker, and for local installs, that walked-up package.json
could even be the USER's own project, reporting a wrong version. Fix:
readHostVersion() (capability-loader.cts, capability-source.cts) and capHostVersion()
(gsd-tools.cjs) now prefer the authoritative gsd-core/VERSION the installer already
writes for EVERY runtime (mirrors resolveVersionFrom(), #1383), falling back to the
runtime-root package.json for the dev/source tree, then fail-closing. This fixes
every runtime and the actual `capability install` CLI path without touching the
marker package.json, so uninstall is unchanged (no data-loss surface).
Gap 2 — scripts/gen-capability-registry.cjs (+ its sibling
gen-loop-host-contract.cjs) were never copied by the installer, so the loader's
never-crash invariant discarded every overlay and fell back to the frozen
first-party registry (installed capabilities silently inert). Now copied,
uninstalled, and manifest-tracked exactly like fix-slash-commands.cjs (#1223).
Regenerates the 16 golden-install-parity fixtures to capture the two newly shipped
generator scripts and the gsd-tools.cjs change.
Regression tests (RED→GREEN): readHostVersion VERSION-first / fallback / fail-closed
resolution; an end-to-end `capability install` against a REAL installed layout
proving the engines gate sees the real host version, not 0.0.0; and a real-install
check that both generators are shipped and manifest-tracked.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The SDK entry (src/host-integration-sdk.cts) is the single PUBLIC surface a
host-plugin author imports: the negotiated schema + classification, the five
adapters (declarative/imperative/model/hook/state), and the serialized
handshake. Frozen so the public shape cannot be mutated. Everything else in
gsd-core stays internal.
tests/sdk-smoke.test.cjs imports ONLY from the SDK entry and builds a
third-party host-plugin end-to-end (compose adapters + handshake + classify) —
proving an external author can wire a host without gsd-core internals (#1683 AC).
ESLint-ignore + inventory manifest kept in sync for the new tsc-emitted module.
* feat(#1683): serialized capability-exchange handshake — Phase 6 Slice 1
The out-of-process wire form of Phase 1's negotiateHostCapabilities: SDK hosts
(pi, VS Code) that cannot share object refs exchange a JSON capability set over
a wire boundary (MCP-style initialize). src/handshake-serialized.cts provides
buildHandshakeRequest (host side) + handleHandshakeRequest (engine side,
delegates to negotiateHostCapabilities), both JSON-round-trip-safe.
CONSISTENCY is the contract: a serialized request yields the same
NegotiationResult as the in-process call for the same axes (asserted in the
test). Pure + additive; the companion MCP server or an SDK host binds it to a
real transport.
* fix(#1683): ignore tsc-emitted handshake-serialized.cjs (ADR-457) + refresh inventory manifest
* fix(#1683): correct inventory manifest — drop junk smart-entry.cjs, add handshake-serialized.cjs
The local gen-inventory-manifest scan captured a stale untracked smart-entry.cjs
(not on next, no src/*.cts) and missed the freshly-built handshake-serialized.cjs.
Aligned to the canonical clean-build report: + handshake-serialized.cjs, - smart-entry.cjs.
* fix(#1683): type the JSON wire round-trips (no-unsafe-return)
* feat(#1933): VS Code IDE reference host binding — completes Phase 5 IDE profile
The reference VS Code host binding composes the Phase-3 engine seams for the
ide profile (host-integration.cts PROFILE_BASELINES): active model via vscode.lm
(createModelAdapter active + sendRequest), engine-owned hook bus
(createHookBus engine — VS Code has no host bus), sandboxed-storage stateIO
(createStateIO sandboxed-storage + host backend — no fs / no child_process),
and the imperative adapter (engine-as-library). Command surface: palette/chat.
VS Code is extension-distributed (Marketplace), not file-projected onto a config
dir, so it intentionally has NO runtime descriptor / --vscode installer entry —
the extension IS the host (architecturally N/A for the descriptor/installer
model, not a deferral). Mock-friendly binding (vscode.lm + hostStorage injected)
so it is behaviorally testable without a live VS Code host.
Tests: IDE profileOf classification; full seam composition (active model routes
to vscode.lm; engine bus pub/sub; sandboxed state routes to backend; imperative
adapter; command surface); fail-closed construction.
* fix(#1933): correct vscode fail-closed test case matrix
Proves the Programmatic-CLI reference binding for pi via the ExtensionAPI
imperative adapter (#1682 AC): createImperativeAdapter({runtime:'pi'}) classifies
as imperative + composes the registry; pi axes (imperative + bun) classify as
'programmatic-cli'; the reference pi host-plugin binds GSD via ExtensionAPI
(registerCommand /gsd + registerTool gsd_invoke + on tool_call).
Reference plugin (tests/fixtures/pi-host-plugin.cjs) is CJS + mock-friendly so
it is behaviorally testable without a live pi runtime. Full --pi installable-
runtime integration (descriptor + installer + golden parity 16→17) is a larger
follow-up — intentionally NOT added to the runtime registry here.
Locks Antigravity as the Declarative-CLI reference host through the public
Host-Integration Interface (#1682 AC): profileOf(antigravity axes) ===
'declarative-cli'; createDeclarativeAdapter({runtime:'antigravity'}) classifies
as kind:'declarative'; a real install emits a gsd command/skill surface (nested
gsd-ns-* router layout). Byte-identity vs today's install stays gated by
golden-install-parity (all 16 runtimes) + adapter-declarative-equivalence.
* feat(#1682): OpenCode session.idle + opencode-subset dialect + Claude parity — Slice 1b/c
- Plugin (.opencode/plugins/gsd-core.js): handle session.idle (↔ Claude Stop
lifecycle point; no-op sentinel — state already persisted to .planning/).
Completes the compaction/idle pair (#1914 shipped compaction).
- Declare hookEvents: 'opencode-subset' in the OpenCode descriptor — the
reserved dialect now has a real consumer (no longer zero-consumer).
- host-integration.cts: add HOOK_EVENT_SURFACES + hookEventSurfaceFor() — the
pure consumer that resolves a dialect to its host-fireable event surface.
opencode-subset = session/tool/file subset with NO workflow-phase events
(engine owns phase sequencing; ADR-1239 §OpenCode binding).
- Tests: hookEventSurfaceFor unit tests (claude/gemini/opencode-subset/null);
plugin session.idle no-throw; compaction breadcrumb; opencode-subset surface
parity vs the plugin's handlers (Claude parity).
* fix(#1682): don't declare hookEvents on opencode (hooksSurface:none invariant)
The repo invariant couples runtime.hookEvents to the managed settings.json hook
surface: hooksSurface:'none' runtimes (opencode — plugin owns hooks) must NOT
declare hookEvents. Declaring 'opencode-subset' there violated 3 capability-
registry invariants + 2 install-plan golden masters + opencode golden parity.
The opencode-subset dialect is still IMPLEMENTED — just not via the legacy
descriptor field: hookEventSurfaceFor() (host-integration.cts) is its consumer,
and the OpenCode plugin consumes the subset events at runtime (session.idle
added here; compaction shipped in #1914). Declaring it on the descriptor would
require weakening the hooksSurface:none ⇔ no-hookEvents invariant (flagged for
decision).
* fix(#1682): refresh opencode golden parity for plugins/gsd-core.js (session.idle)
* docs(changeset): OpenCode session.idle + opencode-subset dialect (#1682)
* docs(changeset): backfill PR #1930
* feat(#1914): OpenCode native plugin integration (Option 1 file-copy)
Ship a native OpenCode plugin (.opencode/plugins/gsd-core.js) plus the
installer step that delivers it, so GSD's lifecycle hooks run on OpenCode.
OpenCode declares hooksSurface:'none', so GSD's hook scripts already ship to
<configDir>/hooks/ but nothing invokes them; the plugin bridges OpenCode's
event bus onto those scripts as subprocesses (prompt/read/worktree/workflow
guards, injection scanner, context monitor).
Distribution is Option 1 (file copy) per the #1914 triage decision: no
scripts.build rename, no prepare/prepack removal. package.json gains
main + .opencode in files[] for discovery.
Corrected against OpenCode's docs + loader source (not the reference branch):
- Auto-discovery globs {plugin,plugins}/*.{ts,js} — .cjs is never matched, so
the installed adapter must be .js (config dir carries {"type":"commonjs"}).
- No opencode.json plugin-array patch — that array is npm-only; local files
are auto-discovered.
- REPO_ROOT is resolved by walking up to the dir holding hooks/ + gsd-core/,
correct for package tree, global install, and local install.
- Config-hook registration is gated (IS_PACKAGE_TREE) so it never
double-registers commands/agents/skills already delivered by native copy.
Also fixes an incidental .gitignore drift: 8 ADR-1239 .cts-generated .cjs
artifacts were untracked-and-not-ignored (leak risk) — now ignored.
Tests: tests/opencode-plugin-adapter.test.cjs (14, pure helpers + real
subprocess bridge against stub hooks); golden-install-parity regenerated.
Green on Mac + Linux (gsd-test): 0 failures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1914): harden OpenCode plugin export shape + advisory accumulation (adversarial findings)
Address Codex adversarial-review findings:
- HIGH: export `{ id, server }` could trip OpenCode's loader
(`for (entry of Object.values(mod)) getServerPlugin(entry)` throws on a
non-extractable value). Make `id` NON-ENUMERABLE and assign module.exports
from a variable (not a literal) so no string `id` is ever iterated — verified
loader-safe under real import(pathToFileURL) (default + module.exports alias,
both objects with .server; no bare id string).
- MEDIUM: sequential advisory hooks clobbered output.metadata._gsdAdvisory;
now accumulate into an array.
- LOW: resolveRepoRoot fallback returned ".." while the comment said "../.." —
aligned to "../.." (package-tree depth).
Tests: added a faithful loader-loop emulation (raw CJS + ESM namespace views),
advisory-accumulation, and a real bin/install.js copy→manifest→uninstall
integration test. golden regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(#1914): add changeset fragment for OpenCode plugin integration (PR #1923)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1914): Windows — assert rewritten path with string include, not path-regex
The Read content-rewrite test built a RegExp from `path.join(root,'gsd-core')`.
On Windows the backslashes in the path are interpreted as regex escapes, so the
assertion never matched and `test (windows-latest, 24)` failed — even though the
adapter rewrote the path correctly. Replace the RegExp with a separator-agnostic
`String.includes` check (the repo's no-path-literal-in-assert concern).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1863): use named flags for state.* calls in executor + workflows
The named-only state-command router (parseNamedArgs) silently drops
positional args, so state.cjs threw its required-arg error and
metrics/decisions/blockers/session continuity were never recorded.
Convert record-metric / add-decision / add-blocker / record-session in
agents/gsd-executor.md to the named-flag form (mirroring execute-plan.md),
and fix the two remaining positional record-session calls in
gsd-core/workflows/milestone-summary.md and forensics.md. Recapture the
golden-install-parity fixtures and size baselines for the edited files.
Also fix a pre-existing detached-rebuild handle leak in
tests/graphify-auto-update.slow.test.cjs: three dispatch tests returned
after observing only the synchronous "running" status without awaiting the
detached rebuild's terminal state. That leak was latent until the new
#1863 regression block's added runtime shifted --test-force-exit timing
and surfaced it as a non-zero chunk exit. The three tests now await
terminal status via the file's existing waitForBuildStatus helper.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(#1863): add changeset
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>
The bug-1367 install test ran install.js without building hooks/dist (a
gitignored build artifact). The unit lane's ensureBuiltArtifacts() builds only
bin/lib, not hooks — so on a lane without pre-built hooks the installer's
verifyInstalled(hooks) found the dir empty and hard-failed 'directory is empty',
throwing in before() → hookFailed → L0-L5 cancelledByParent cascade on the
Windows CI lane.
Build hooks in before() via scripts/build-hooks.js (mirrors golden-install-parity's
BUILD_SCRIPT pattern). Self-contained — no lane-ordering dependency.
Closes#1926
* fix(#1716): route resume_from_file to complete_session when no pending tests remain
When a UAT session has status:partial with blocked_count>0 and pending_count==0 (all remaining tests are blocked, none are pending), resume_from_file found no [pending] test and terminated silently — never routing to complete_session. This blocked the issues==0 auto-transition path even when there were zero code defects.
Guard clause added immediately after the find-pending step: if no [pending] test is found, route to complete_session. complete_session then correctly sets status:partial (because blocked_count>0) without presenting further tests.
Closes#1716
* chore(#1716): add changeset fragment and regenerate golden-install-parity fixtures
Changeset fragment for PR #1722 (type: Fixed).
Golden-install-parity fixtures regenerated for all 16 runtimes — the workflow fix shifts verify-work.md's byte-stable hash in the golden manifest. Regenerated via UPDATE_GOLDEN=1 node --test tests/golden-install-parity.test.cjs.
* fix(#1907): validate per-item shape in isValidReport so adapter garbage fails closed
isValidReport checked only the container (items is-array, coverage scalars), so a report
like {items:[{}]} sailed through runProbeCli and stringified as green output — despite the
docstring promising it 'fails closed on adapter garbage'. Add a per-item Item-contract guard
(requirement_id/category/status + typed nullable fields) so a future adapter that bypasses
the analyzeCoverage merge and returns per-item garbage inside a well-shaped envelope fails
closed (exit 2) instead of emitting it as valid coverage.
analyzeCoverage always emits fully-populated, validated Items, so the 2 shipped adapters are
unaffected (verified across the edge/prohibition suites).
Refs #1907, epic #1904.
* chore(changeset): Fixed fragment for #1910 (isValidReport per-item)
* fix(#1905): normalize hand-authored backstop marker so it can't degrade to green
A hand-authored non-inferable `backstop` truth with a stray trailing space (or
surrounding quotes) silently graded {status:green} instead of abstaining — the exact
#1154 false-pass. `truthVerification` returned null for any value != 'backstop'/'explicit',
and the frontmatter continuation-KV parser preserved the stray whitespace captured inside
the quotes. Normalize the marker before comparison (Postel) AND trim the continuation-KV
value at the parser (durable root cause; also cleans the sibling check_target path).
Regression: a trailing-space/quoted backstop truth now abstains (insufficient_spec),
end-to-end from a hand-authored must_haves.truths block (#1820 rail).
Refs #1905, epic #1904.
* chore(changeset): Fixed fragment for #1909 (backstop marker normalize)
* fix(#1729): resolve phase headers with a pre-colon parenthetical tag
A phase header may carry a parenthetical tag between the number and the
colon, e.g. `### Phase 26 (Cluster B): Title`. Every phase-header regex
built `Phase\s+<num>` immediately against the colon delimiter, so the
tagged phase was invisible: the resolver returned found:false and, just
as bad, the capture-all enumeration/parse paths (roadmap analyze,
milestone listing + milestone-scope filter, verify, init/import, state
total_phases, validate, the command router, preamble stripping, and the
phase-remove renumbering rewrite) silently dropped, miscounted, or
failed to renumber it — wrong phase_count, progress_percent, next_phase,
or corrupt numbering after a removal.
The fix tolerates the tag at the header seam. Parameterized resolver
sites compose the exported OPTIONAL_PHASE_TAG_SOURCE fragment; literal
enumeration sites inline its character-for-character mirror
`(?:\s*\([^)\n]*\))?`, placed immediately before the colon so it cannot
alter an existing match (optional, single-line, one paren pair, no
capture-group shift). In the renumber-on-removal rewrite the tag is
folded into the re-emitted suffix capture so it survives verbatim. Both
forms are documented to change together and a drift-guard test asserts
their behavioral equivalence over a header corpus.
Deliberately excluded: roadmap-upgrade.cts (legacy one-time migration),
where tolerating the tag would silently drop it on header rewrite — that
needs its own data-preserving treatment. Known boundaries left for
follow-up: checklist/bullet-style phase entries (`- [ ] Phase N (tag):`)
and a malformed space-before-colon variant, both pre-existing.
Validated empirically against the issue's reproduction: `roadmap
get-phase 26` resolves and `roadmap analyze` lists Phase 26 with the tag
excluded from the name (phase_count 2, next 26); an all-tagged versioned
roadmap now scopes correctly instead of falling back to a pass-all
filter. Regression coverage in tests/phase.test.cjs asserts resolver
parity (pre- vs post-colon), padding tolerance (#3537), decimal
sub-phases, no cross-phase false match, the shared seam, enumeration
coherence, renumber-preserves-tag, and seam/mirror drift. Full unit
suite green (7291 pass, 0 fail); eslint + regression-name +
resolution-provenance + changeset lints pass. Reviewed by Codex
(no critical/high; the two enumeration misses it surfaced are folded in).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(#1729): add changeset for pre-colon phase-tag fix
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(#1779): emit valid YAML for unsafe scalars in reconstructFrontmatter
reconstructFrontmatter wraps a scalar or block-array item in double quotes
when it contains a YAML indicator (`:`/`#`, plus `[`/`{` for top-level
scalars), but interpolated the raw value with no escaping. A value carrying
both an indicator and a literal `"` (or `\`) serialized to invalid YAML, e.g.
`upstream: "https://x (Tom; "Git. Ship. Done")"`, which fails under any strict
parser (js-yaml, PyYAML) and corrupts the whole block on the next
syncStateFrontmatter whole-block regen.
- escapeDoubleQuoted() escapes `\`, `"`, and control chars (newline/tab/CR/C0
controls + DEL → YAML `\n`/`\t`/`\r`/`\xHH`) so any wrapped value is valid.
- scalarNeedsDoubleQuoting() routes values that mis-parse or round-trip lossily
when bare through that escaped form: the empty string (bare `k:` reloads as
null), an embedded `"`/`\` or control char, a leading YAML indicator
(quote / `&`*`!` anchor-alias-tag / `|`>` block scalar / flow `[]{},` / `#` /
reserved `%`@`backtick / `-`?`:` before a space), or leading/trailing space.
Applied at all four wrap sites.
Scope stays on serialization correctness; it does NOT broaden the lossy
object-list handling deferred to #1572/#1660. Known limitation: lone UTF-16
surrogates are still lossy through UTF-8 encoding (out of scope, extremely
unlikely in frontmatter values).
Regression test asserts strict js-yaml round-trip across all four wrap sites
plus backslash, control-char (incl. NUL), empty-string, leading-indicator, and
leading/trailing-whitespace classes; test-the-test confirms each fails on the
unescaped output. Frontmatter suite 549/549 green, golden-install-parity 16/16
unchanged (no serialization churn).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(#1779): add changeset for frontmatter quote-escaping fix
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to #1919 (archive-then-remove core). Closes the remaining #1871
acceptance criteria so phase history is preserved across the full milestone
lifecycle, not just at phases.clear:
- #2 src/milestone.cts + src/phases-command-router.cts: extract shared
archivePhaseDirectories() helper; add cmdPhasesArchive (the previously
half-wired phases.archive alias now routes instead of erroring Unknown).
- #4 gsd-tools.cjs + src/milestone.cts: milestone complete archives phase
dirs by default (--no-archive-phases opts out; --archive-phases is now a
harmless no-op). complete-milestone.md updated to drop the redundant manual
Yes/Skip archive prompt.
- #3 gsd-core/workflows/new-milestone.md: §6 stages the archive move + source
removal (git add .planning/milestones/ .planning/phases/) in the same commit
as the milestone start, so the archive lands atomically — no orphaned
uncommitted deletions, no un-archived dirs inherited.
- docs/CLI-TOOLS.md (+ ja/zh/ko/pt) + help/modes/full.md: flag accuracy.
- tests: phases archive command (#2) + milestone complete default archive /
--no-archive-phases opt-out (#4). Goldens + workflow size baseline refreshed.
Closes#1871
cmdPhasesClear hard-deleted committed phase directories (rmSync) with no
archive, so browsable phase history was silently lost at a milestone switch.
The #1447 dirty-tree guard was a no-op for the common committed case (a clean
tree passes the guard, then rmSync destroyed the dirs, leaving orphaned
uncommitted deletions and no archive).
Archive-then-remove: move each non-999 phase dir to
milestones/<version>-phases/ (version from getMilestoneInfo; timestamp
fallback; collision-safe) using retryRenameSync — mirroring the existing
archivePhases path in cmdMilestoneComplete. The #1447 uncommitted-changes
guard is retained as a secondary backstop.
Tests in tests/new-milestone-clear-phases.test.cjs updated: phase content is
asserted to SURVIVE in milestones/*-phases/ (archived), not be destroyed —
including the committed-dirs case that previously codified the no-op.
Closes#1871
cmdMilestoneComplete hardcoded three archive paths to root .planning/
while its siblings (roadmap/requirements/state/phases) used workstream-aware
planningPaths. So `milestone complete <v> --ws <name>` scattered its archive
into root and never created workstream-local milestones/.
Derive the archive base from the workstream-aware planning root:
- milestonesPath / archiveDir / auditFile now use planningPaths(cwd).planning
- flat mode (no --ws) is a no-op (planningPaths(cwd).planning == root .planning)
Regression in tests/milestone.test.cjs: --ws archives into the workstream
milestones dir, not root.
Closes#1911
cmdInitProgress used planningDir(cwd), which resolves to root .planning
when no active workstream and no --ws/GSD_WORKSTREAM is set — regardless
of mode:workstream. So /gsd-progress confidently reported a stale root
milestone with no signal it was stale.
Fail safe: when .planning/workstreams/ has workstreams AND no active
workstream is resolved, error with an actionable hint naming the available
workstreams and the --ws / `workstream set` fix. Flat mode (no workstreams
dir) and --ws <name> are unchanged.
Regression in tests/init.test.cjs: errors when workstreams exist but none
active (no stale root report); succeeds with --ws; flat mode unchanged.
Closes#1912
workstream progress trusted the mutable STATE.md `Status` field, so a
shipped/archived milestone whose field was left at `executing` was reported
as executing — a stale hand-maintained field became the source of truth
instead of the authoritative archive/tag/ROADMAP signals.
Derive status in the inventory builder from a milestoneShipped signal
(archived milestone snapshot under milestones/, or a SHIPPED marker in the
workstream ROADMAP), collected by inspectWorkstream. The inventory now
reports `status_source` (field|derived) and `status_conflict` (true when
the derived value disagrees with the stale field), and a shipped workstream
is never reported executing.
- src/workstream-inventory-builder.cts: milestoneShipped input + status_source/status_conflict outputs + derivation
- src/workstream-inventory.cts: workstreamMilestoneShipped() signal detector wired into inspectWorkstream
- tests/workstream-inventory.test.cjs: regression (builder unit + inspectWorkstream integration + negative)
Closes#1913
Each of the 22 consumer agents now self-loads its configured agent_skills
in its mandatory init step, so .planning/config.json agent_skills.<type>
reaches the agent on every runtime — including Cursor and /gsd-autonomous,
where Skill()-delegated workflow bash init did not reliably execute.
- gsd-core/references/agent-skills-bootstrap.md: shared contract
(query + Read + dedup guard that skips when <agent_skills> is already
in the prompt, so Claude's orchestrator-side injection never doubles)
- 22 agents/gsd-*.md: one self-load line naming the agent's own type
- gsd-core/workflows/autonomous.md: note that delegated agents self-load
- tests/agent-skills-bootstrap.test.cjs: regression + parity (CONSUMER_AGENTS
bijection + fast-check property) — Generative-Fix-Divergence guard
- docs: ADR-1866, CONFIGURATION dual-injection How It Works, INVENTORY
row, Changed changeset
Closes#1866
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
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>
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>
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)
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)
* 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>
* 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>
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>
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>
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>
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).
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.
* 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>
* 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.