Add a curated 1.7.0 release-highlights page (docs/whats-new-1.7.0.md) and a
conceptual Embeddable Orchestration System (EoS) explanation
(docs/explanation/embeddable-orchestration-system.md), extend docs/FEATURES.md
with a v1.7.0 feature section, and wire both new docs into the docs index
(docs/README.md) and the root README.
Covers the release's marquee changes: the ADR-1239 Host-Integration Interface /
EoS (Embeddable Orchestration System) runtime expansion, the Capability + EoS
discoverability registries, the gsd-mcp-server companion, model-catalog advances
(GPT-5.6, (1M) badge), statusline enhancements, the compact GSD-state format,
plus a themed summary of the 100 fixes and 4 security hardenings.
Also corrects a stale CONTEXT.md glossary entry: the Capability Registry Overlay
now documents the #2009 fail-open behavior for a load-failed gate-declaring
capability (previously described as fail-closed).
American house style; no parity-gated reference docs hand-edited.
Refs #2276, #1678
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#2162): opt-in compact GSD-state format for the statusline
New statusline.state_format config, enum full|compact (default full —
existing rendering untouched). "compact" renders the state segment as
"<version> · P<phase>/<total> · <status>", e.g. "v1.12 · P7/12 ·
executing" — dropping the milestone name and progress bar (the two
biggest width costs) and collapsing narrative statuses to a single
keyword. Per the #2162 approval conditions, the keyword set is the
canonical vocabulary from normalizeStateStatus() in state-document.cjs
(discussing/planning/executing/verifying/completed/paused) — no
parallel hand-rolled list, so the vocabularies can't drift — and the
canonical stuck state "paused" renders uppercase as PAUSED (no new
"blocked" lifecycle state). Statuses the normalizer passes through
unrecognized fall back to their first word capped at 16 chars.
Lifecycle scenes preserved: active_phase wins over the body phase
number, milestone completion renders "complete", idle-with-next-action
renders "next <action> <phases>".
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* docs(#2162): changeset fragment for PR #2175
* fix(#2162): review fixes — ENUM_KEYS coverage, cap boundary tests, changeset format
- register statusline.state_format in the fix-1628 coercion-bypass matrix
- 15/16/17-char boundary tests for the shortGsdStatus fallback cap
- changeset body ends with the (#2162) citation per house convention
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* fix(#2162): round-2 review fixes — scene exclusivity, direct config-set coverage
- compact renderer gates the milestone-complete scene behind the absence of
an in-flight phase id, mirroring formatGsdState's if/else precedence
(Scene 1 beats Scene 3); regression test covers the non-atomic
active_phase + percent=100 STATE.md shape
- direct config-set accept/reject test for statusline.state_format plain
strings (ENUM_KEYS matrix covers only the JSON coercion shapes)
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* test: regenerate golden-install-parity fixtures for the statusline hook change
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* fix(#2162): complete-scene gate matches formatGsdState exactly (+property tests)
Re-review Major: gating done on !phaseId held completion back for the
legacy phaseNum shape — formatGsdState reaches Scene 3 on percent=100
regardless of phaseNum, so compact must too. Gate is now !s.activePhase.
The phaseNum-only test now expects 'complete' and cross-checks the full
renderer; a parity test feeds identical inputs to both renderers.
Re-review Minor: shortGsdStatus gets fast-check property coverage
(totality, canonical fixed points, separator safety, fallback shape).
Golden fixtures regenerated for the hook byte change.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* feat(#2160): collapse verbose '(1M context)' model suffix to compact (1M) badge
Claude Code appends " (1M context)" to the model display name in
long-context sessions, eating 12 characters of statusline width. Collapse
it to " (1M)" — the signal stays, the width doesn't. Any other display
name passes through unchanged.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* docs(#2160): changeset fragment for PR #2173
* fix(#2160): review fixes — ctx variant, boundary test, changeset format
- broaden the suffix match with a context|ctx alternation (approval-condition
variant the regex missed)
- pin non-context parentheticals ((beta), (deprecated)) as untouched
- changeset body ends with the (#2160) citation per house convention
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* test: regenerate golden-install-parity fixtures for the statusline hook change
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
---------
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
Phase 3 (convenience form) of golden-parity redesign (epic #2264). Adds npm run gen:golden (regenerates both fixture sets) and points the golden-parity/tree failure messages at it. Full CI-auto-comment deferred (documented in ADR-2264). Closes#2268.
Phase 2 of golden-parity redesign (epic #2264). Adds an install file-set snapshot (golden-install-tree) and a ci-test-scope rule selecting golden-parity whenever any installed-source path changes, closing the silent-staleness hole behind the #2266 red. ADR-2264 amended (the copy/transform split premise was unsound). Closes#2267.
* fix(#2237): fail loud on ambiguous bare-number phase directory collision
When two unrelated projects share a .planning/phases/ tree, a bare phase
number silently resolved to the first 0N-* directory found — risking
cross-project file writes. The fix detects multiple matches for the same
phase number and surfaces an ambiguous_matches result instead of silently
taking the first.
Changes:
- src/phase-locator.cts: searchPhaseInDir uses filter() + ambiguity check
- src/phase.cts: cmdFindPhase same pattern
- src/init.cts: cmdInitPhaseOp surfaces ambiguous_matches in the result
- tests/phase-locator.test.cjs: 3 regression tests
* docs: backfill changeset PR number (#2262)
* merge: keep up to date with next
* fix: regenerate stale capability-registry after next merge
* fix(#2236): add hookShell parameter for PowerShell call operator
Windows Claude Code with a PowerShell hook runner failed on every hook
with 'Unexpected token' — the installer emitted bare quoted paths that
PowerShell parses as string literals, not commands.
The root cause was that hookCommandNeedsPowerShellCallOperator returned
false unconditionally — the &/no-& decision was keyed on (platform,
runtime) only, with no signal for the effective hook-execution shell.
One runtime (Claude Code) hosts either Git Bash or PowerShell on Windows.
Fix: thread a new hookShell parameter through the projection chain.
When hookShell='powershell', the & call operator is prepended. Default
(Git Bash, no prefix) is unchanged and regression-locked.
* docs: backfill changeset PR number (#2261)
* merge: keep up to date with next
* fix: regenerate stale capability-registry after next merge
* fix(#2252): exclude PLAN-REVIEW artifacts from plan count
The loose /PLAN/i fallback in isRootPlanFile matched *-PLAN-REVIEW.md,
inflating plan counts. Added PLAN_REVIEW_RE exclusion before the fallback.
* docs: backfill changeset PR number (#2263)
* fix: regenerate stale capability-registry after next merge
* fix(#2220): replace invalid 'mine --room' with detect_room() staging approach
mempalace mine has no --room flag (only search does) — verified against
MemPalace 3.5.0 official docs (mempalaceofficial.com/reference/cli.html).
The headless capture path used --room, causing every headless/no-MCP run
to fail with 'unrecognized arguments: --room' and silently skip capture.
Fix: replace the flag with a staging-based approach that uses detect_room()'s
documented folder-path match — stage the artifact under a room-named subfolder
with a mempalace.yaml room taxonomy, then run 'mempalace mine <stage> --wing'.
Docs sources cited in-file:
- CLI reference: https://mempalaceofficial.com/reference/cli.html
- Mining guide: https://mempalaceofficial.com/guide/mining.html
- Config guide: https://mempalaceofficial.com/guide/configuration.html
Changes:
- skills/gsd-mempalace-capture/SKILL.md: headless staging instructions
- commands/gsd/mempalace-capture.md: same
- capabilities/mempalace/fragments/capture-problems.md: reference staging
- .gitignore: exclude .planning/.mempalace-stage/
- tests/mempalace-capture-headless-invocation.test.cjs: regression test
- Golden install parity fixtures + workflow-size baseline regenerated
* docs: backfill changeset PR number (#2260)
* fix: regenerate golden fixtures after next merge
* fix(#2204): phase-completion writes 'All phases complete' per ADR-2207
completePhaseCore was writing the overloaded bare 'Milestone complete' on the
last phase — the same string space the milestone-close verb owns for terminal
state. Per ADR-2207, phase-completion now writes the existing intermediate
value 'All phases complete' (already used in gsd2-import.cts). Milestone
termination ('<version> milestone complete' / 'Awaiting next milestone')
remains solely with milestoneCompleteCore.
Status lifecycle: Ready to plan → All phases complete → <version> milestone
complete → Awaiting next milestone.
Changes:
- src/state-transition.cts: completePhaseCore status value
- src/phase.cts: #2028 guard comment
- tests/state-transition.test.cjs: assertion + test name
- tests/phase.test.cjs: 8 assertion updates (positive + negative)
- tests/state.test.cjs: normalizeStateStatus test case + reset regex
- tests/workstream.test.cjs: fixture status to terminal value
- gsd-core/workflows/progress.md: Route D label
- gsd-core/workflows/transition.md: Route B label
- CONTEXT.md: Status lifecycle glossary entry (ADR-2207)
- .changeset/brave-geese-jump.md
* test(#2204): regenerate golden-install-parity fixtures + workflow-size baseline
Workflow file edits (progress.md, transition.md) changed install payload
hashes and pushed past the committed workflow-size baseline. Regenerated
all 17 golden-install-parity fixtures + claude-local via the standalone gen
script (which now also covers the local-scope claude layout). Updated
workflow-size-baseline.json and agent-size-baseline.json via size:baseline.
* fix(#2204): correct claude-local golden hashes + document gen-script limitation
The gen-script's claude-local generation produces macOS-specific hashes
incompatible with Linux CI (local-scope install embeds platform-varying
node-runner paths). Reverted to manual update using Linux FAILURES.md
+actual hashes for the 2 changed workflow files. Added explanatory
comment in the gen script.
* test(#2204): add isCompletedInventory coverage + clarify CONTEXT.md glossary
Addresses orthogonal code-review findings (Medium #1 + #2):
- Add isCompletedInventory test cases for ADR-2207 status lifecycle
(terminal 'milestone complete' → true; intermediate 'All phases
complete' → false; archived → true; active statuses → false)
- Clarify CONTEXT.md glossary: note that isCompletedInventory
intentionally excludes the intermediate value
* docs: backfill changeset PR number (#2259)
* docs(#2204): add Status lifecycle table to state-md reference (ADR-2207)
* chore(#2143): prohibition-with-teeth + migrate remaining table sites — Phase 4
Phase 4 of epic #2143 (ADR-2143 §7). Completes the markdown table/mutation
consolidation by (a) giving the ad-hoc-parsing prohibition teeth and (b)
migrating the last ad-hoc table sites onto the shared seam.
- src/markdown-table.cts: new formatting-preserving `updateTableCell` primitive
(self-contained, ragged-row-tolerant header/delimiter/cell-range scan; splices
only the target cell's raw span, preserving all other bytes incl. padding/CRLF;
no-op-preserves-padding when a transformer returns the current value). Exports
splitTableRow/isDelimiterRow/findTableStartOffset for tolerant reuse.
- eslint-rules/no-adhoc-markdown-parsing.cjs: TABLE-REGEX detector extended to
`new RegExp(<literal|static-template>)`; new `.replace()`-mutation detector for
roadmap/state/content receivers with a table/section-shaped pattern.
- scripts/lint-table-schema-drift.cjs (wired into lint:ci): fails if a TABLE_SCHEMA
header drifts from its authored table; tests import its logic (single source).
- Migrated onto the seam (behaviour-preserving vs pre-Phase-4 HEAD, verified
byte-diff old-vs-new): roadmap.cts cmdRoadmapUpdatePlanProgress, phase.cts
cmdPhaseComplete + traceability, milestone.cts cmdRequirementsMarkComplete,
uat.cts read path, state.cts metrics/decisions/By-Phase.
- Incidental correctness gains from the migration: a decoy table can no longer
swallow a phase-progress update (## Progress scoping); a ragged neighbouring
row no longer silently aborts an edit; completing integer phase N no longer
touches a decimal sub-phase N.x row; record-metric no longer drops trailing
section content or duplicates the ## Performance Metrics section.
- Kept justified allow-adhoc-markdown markers only where genuinely not a table
(security.cts <|role|> token) or a loose non-GFM section (uat human-verify).
Two orthogonal isolated reviews (correctness/adversarial + security) passed;
correctness found 4 behaviour regressions in the first migration pass, all fixed
and re-verified byte-identical-or-better vs OLD.
Surfaced for maintainer (pre-existing, ambiguous domain logic, NOT changed here):
templates/state.md places a By-Phase table under ## Performance Metrics while
cmdStateRecordMetric assumes a Plan|Duration|Tasks|Files table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): match traceability row by first-cell value, not Requirement header
Phase 4's migration matched the REQUIREMENTS.md traceability row by a column
literally named `Requirement` (`row['Requirement']`), but real tables head that
column `REQ-ID`. The by-name lookup found nothing, so `phase complete` and
`requirements mark-complete` left the Status cell `Pending` (regressed #2769 /
#2203, caught by gsd-test — 8 failures, both node 22/24).
- src/phase.cts, src/milestone.cts: match the row by its FIRST cell's value
(the requirement-ID column) regardless of that column's HEADER name, via
`Object.values(row)[0]` (updateTableCell builds the record in header order).
This mirrors OLD's first-cell `\|\s*<id>\s*\|` anchor, restoring header-name
independence while keeping the seam.
- src/milestone.cts hasTable: broadened from `Requirement`-only to also
recognize `Requirement ID` / `REQ-ID` / `REQ ID` headers, kept in sync with
the now-positional rowMatch/hasRow so a REQ-ID-headed table participates in
the ADR-2143 §6 write-set and the #2140 table_unmatched drift check (it was
silently omitted before — a checkbox-only partial reconcile against a REQ-ID
table could report as fully reconciled). The `Requirement`-headed path is
byte-identical to OLD.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#2143): replace stale structural milestone guards with behavioural suite
The `milestone.cjs regex global state fix` block was a source-structure guard
(allow-test-rule: structural-regression-guard) — it readFileSync'd the compiled
milestone.cjs and asserted removed regex idioms (`tablePattern.test`,
`afterTable !== reqContent`, `doneTable = new RegExp(...)`). Phase 4's migration
deleted those regexes (table update is now updateTableCell), making the
assertions obsolete. Per the Test Cleanup rule, replace them in-PR with a
behavioural suite driving the compiled CLI:
- multi-ID mark-complete flips all IDs (guards the lastIndex/global-state class),
- Pending->Complete flip under both `REQ-ID` and `Requirement` headers (#2769),
- idempotent already_complete detection with no corruption,
- REQ-ID-headed table participates in write_set (traceability entry, applied),
- REQ-ID-headed table trips #2140 table_unmatched drift on a missing row.
Pruned the now-nonexistent structural-regression-guard entry from the
lint-allow-test-rule-refs allowlist (the source-text-is-the-product entry for
the same file remains valid).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(changeset): backfill PR number 2253
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): record-metric targets its own metrics table, not By-Phase velocity
`state record-metric` appended its per-plan row (`| Phase 1 P1 | 5min | 3 tasks |
4 files |`) into the FIRST table under `## Performance Metrics` — which on a real
template-derived STATE.md is the By-Phase velocity table `| Phase | Plans | Total
| Avg/Plan |`, polluting it on EVERY plan completion (execute-plan.md:414 is a
per-plan call). The command's own metrics table is `| Plan | Duration | Tasks |
Files |`, which the template does not ship, so the row never reached it; the
scaffold branch also emitted a wrong `| Phase | Plan | Duration | Notes |` header
matching neither the row nor the canonical table.
Pre-existing (predates Phase 4); surfaced while migrating this site and fixed here
per no-defer, on the user's explicit go-ahead.
- src/state.cts cmdStateRecordMetric: locate the metrics table by its own header
shape (`Plan|Duration|Tasks|Files`, via splitTableRow/isDelimiterRow) rather
than "first table in the section". When the section exists but has no metrics
table (only the By-Phase table), self-heal by appending a fresh **Per-Plan
Metrics:** table to the END of the section body — By-Phase table, Recent Trend
and footer preserved verbatim, no duplicate `## Performance Metrics` heading,
created stays false. Absent-section scaffold header corrected to the canonical
`| Plan | Duration | Tasks | Files |`. Ragged-tolerance + None-yet preserved.
- Not touching templates/state.md (golden-install-parity hashed) — record-metric
self-creates the table on first use instead.
Failing-first regression test (tests/state.test.cjs) demonstrates the By-Phase
pollution on the pre-fix build, then green after. Verified: no pollution, self-
heal idempotency, both-tables isolation, content/heading preservation, flags,
None-yet, corrected scaffold header (23-check adversarial harness + all existing
record-metric scenarios).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#2143): deleteSection seam primitive (level-bounded whole-section removal)
ADR-2143 §4 shipped withSection/collectSection (replace a section BODY) but no
way to DELETE a section (heading + body). Phase 4 suppressed the phase-remove
section delete instead of building it. deleteSection(content, predicate, opts)
locates the section via the collectSection machinery and splices out from the
heading's start offset to the next same-or-higher-level heading — so a level-3
`### Phase N` delete stops at a following level-2 `## Progress`, never past it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): phase remove no longer deletes ## Progress on last-phase removal
updateRoadmapAfterPhaseRemoval deleted a `### Phase N` detail section with a
greedy raw regex whose lazy scan, on the LAST phase, ran to EOF and destroyed
the following `## Progress` heading and its entire tracking table — silent data
loss, uncovered by tests (removal tests only exercised a middle phase). Migrated
onto the new deleteSection seam (level-bounded, stops at `## Progress`); dropped
the allow-adhoc-markdown SECTION-DELETION suppression. Failing-first regression
(tests/phase.test.cjs) removes the LAST phase and asserts the ## Progress heading
+ table survive; middle-phase removal is byte-identical.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#2143): deleteTableRow seam primitive (row removal, ragged-tolerant)
Sibling of updateTableCell: locates the first GFM table, matches a DATA row by
predicate (ragged-tolerant record build, header order), and splices out that
row's whole line preserving every other byte. Returns {ok:false,reason} on no
table / no match. Enables migrating the phase-remove Progress-table row delete
off its ad-hoc regex (ADR-2143 §7 — the "future row-delete seam" Phase 4 punted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): phase remove deletes the Progress row via deleteTableRow
The Progress-table row delete used a whole-document regex with two defects:
(a) `\.?\s` required whitespace after the phase number, so a COMPACT row
`|2|Beta|` was never deleted (stale row left behind); (b) unscoped — it could
strike a row in a different table (e.g. an earlier `| Phase | Requirements |`
table). Migrated onto deleteTableRow, scoped to the `## Progress` section
(mirrors deriveProgressFromRoadmap), matching the row by first-cell phase number
(integer zero-pad-insensitive; decimal exact; removing `2` never touches `2.5`).
Both allow-adhoc-markdown suppressions removed. New behavioural tests: compact
unpadded row deleted; padded byte-parity on the surviving rows (their ordinal
correctly renumbers via the pre-existing renumber block).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): deleteTableRow leaves no dangling newline on last EOL-less row
Deleting the final row of a table with no trailing EOL sliced from the row's
start to end-of-string, stranding the newline that terminated the previous line.
Back rowStart over the preceding \r?\n in that branch so the table ends cleanly.
(Caught by the primitive's own unit test on gsd-test; local scenario checks
missed the no-trailing-EOL edge.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): migrate read-only section-collects onto collectSection
Six hand-rolled `## Section` read-extract regexes replaced by the collectSection
seam (behaviour-preserving; extracted bodies feed the same downstream parsers):
state.cts matchSessionSection (## Session / ## Session Continuity) + ## Blockers,
smart-entry.cts ## Blockers, audit.cts ## Current Focus + ## Open Questions.
Removes 6 allow-adhoc-markdown "pending #1372" suppressions. Incidental fix: the
old Session regex `## Session[ \t]*\n` silently failed on a CRLF `## Session\r\n`
heading (Windows STATE.md), nulling all session fields; collectSection is
CRLF-safe, so session state now resolves on Windows.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): fence-safe state-transition section writes + dedup stripFrontmatter
- milestoneCompleteCore's `## Current Position` and `## Operator Next Steps`
section resets used fence-blind raw regexes that a fenced `##` inside the body
could truncate/mis-target (#2130/#2067/#2080 class). Migrated onto a
fence-aware tokenizeHeadings-based helper (resetSectionVerbatim) that is
byte-identical to the old output on the canonical path (9/9 fixtures) and
correctly ignores a fenced fake heading (proven robustness gain).
- mutateCurrentPositionFirstTime: hand-rolled locate+splice → collectSection +
replaceSection (byte-parity).
- stripFrontmatter was inlined byte-identically in state.cts AND
state-transition.cts; hoisted the single canonical copy into frontmatter.cts
(both call sites now import it) + unit tests — eliminates the divergence risk
per CLAUDE.md "Generative Fix Divergence". Removes 3 allow-adhoc-markdown /
#1372 markers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): name-address By-Phase sum + uat parse, eslint recall hole, catches
- state.cts By-Phase "Total plans completed" sum: positional 2nd-cell regex →
name-addressed splitTableRow read (correct on a reordered header, where the
old code silently summed the wrong column). Marker removed.
- uat.cts parseVerificationItems: loose pipe regex → splitTableRow within the
existing table/numbered/bullet union scan (item list byte-identical; does NOT
reintroduce the reverted strict-parseMarkdownTable item-drop). Marker removed.
- eslint no-adhoc-markdown-parsing: close the `new RegExp(identifier)` recall
hole — resolve a const-declared table-shaped regex identifier (mirrors the
.replace() detector) + RuleTester cases; param/call args stay out (boundary).
- commands.cts: delete a lying comment that claimed the scaffold date "stays on
raw UTC / deferred" — #2136 already moved it to realClock.localToday().
- Empty catches (classified, not blind-swept): removed 4 dead try/catch;
fixed 3 error-hiding (phase-insert decimal-dir I/O collision now fails loud;
phase-remove rename partial-failure surfaced; milestone-archive true count via
finally); left best-effort swallows with justification comments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): extractFencedBlock seam + migrate api-coverage named fence
parseCoverageMatrix extracted its ```coverage fenced block with an ad-hoc regex
(the last real allow-adhoc-markdown suppression). Added extractFencedBlock to the
markdown-sectionizer seam (reuses stripFencedCode's CommonMark fence engine —
info-string match, ~~~/backtick, nesting, indent) and migrated onto it; byte-
parity on the parsed CoverageMatrix across 8 fixtures. Only security.cts:367
(a genuine `<|role|>` protocol-token false-positive, not a GFM table) remains
marked in src/ — the "prohibition with teeth" goal (nothing grandfathered but a
true FP) is met.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): By-Phase row insert is name-addressed (insertTableRow seam)
updatePerformanceMetricsSection's INSERT-new-row branch located the By-Phase
table with a canonical-column-order-only regex + a hardcoded positional row
literal, so on a reordered header it silently inserted nothing — inconsistent
with the now name-addressed UPDATE and SUM halves of the same function. Added
insertTableRow (markdown-table seam sibling of updateTableCell/deleteTableRow:
name-addressed, header-order-agnostic, EOL-preserving) and migrated the branch
onto it, mapping By-Phase values by column NAME. Canonical-order output is
byte-identical; a reordered header now inserts a correctly-mapped row; a
pre-existing CRLF mixed-EOL splice glitch is incidentally fixed. Retired the
now-dead byPhaseTablePattern const.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): phase-list checkbox flip via updateBullet seam
Added updateBullet (markdown-sectionizer): a fence-aware, offset-tracked
single-bullet write primitive (GFM 1–4-space marker tolerance) — the write
counterpart to read-only iterateBullets. Migrated mutateMilestonePhase's
phase-list checkbox flip (`- [ ] Phase N …` → `- [x] … (completed <date>)`)
off its whole-slice regex onto it, same milestone-slice scope + clock seam.
Byte-identical across simple / idempotent / metachar-title / double-space /
CRLF scenarios.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): scope the Progress-ordinal renumber to ## Progress via seam
phase remove's integer-renumber decremented Progress-table phase ordinals with a
whole-document `content.replace(/(\|\s*)(\d+)(\.\s)/g, …)` — unscoped, so it also
rewrote any `| N. …` cell in an unrelated/decoy table (same class as the batch-2
row-delete scoping bug). Migrated onto updateTableCell, scoped to the ## Progress
section, decrementing each affected row's leading phase ordinal by column name.
Byte-identical on canonical Progress tables + multi-row + decimal-sibling cases;
a decoy `| 3. … |` row before ## Progress is now correctly left untouched. The
sibling heading / checkbox-bullet / PLAN.md-filename / Depends-on-prose renumbers
are not GFM-table mutations (outside ADR-2143's table/section mandate) — left as-is.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2143): review fixes — scope traceability write, restore Current Position H3-stop
Adversarial review of the remediation (BLOCK verdict) — all 9 findings fixed:
- F1 (BLOCKER): requirements mark-complete / phase complete flipped the checkbox
but NOT the traceability row on the shipped template, because updateTableCell
bound to the FIRST table (## Out of Scope, no Status column) instead of the
## Traceability table — the #2140 silent-divergence class, re-introduced by the
seam migration and missed by tests (fixtures had Traceability first). Scoped
the write + hasRow probe to the ## Traceability section slice (updateTraceability
Cell helper) in milestone.cts + phase.cts. Failing-first tests on the
Out-of-Scope-before-Traceability layout; the #2769 first-cell match preserved.
- F2 (MAJOR): mutateCurrentPositionFirstTime restored to locateCurrentPosition
(STOP_H2_PLUS) — collectSection's default H2-stop swallowed a level-3 subsection
and the field regexes clobbered it (#2130 class).
- F3/F8: Progress-ordinal renumber re-escapes via escapeCell + keys padding
recovery by row index (was de-escaping `\|` and losing padding on dup values).
- F4: insertTableRow escapes cell values internally.
- F5: updateBullet accepts a tab after the marker (`[ \t]{1,4}`).
- F7: resetSectionVerbatim consumes CRLF blank lines (byte-parity on CRLF).
- F6/F9: corrected two misleading comments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(changeset): data-loss + CRLF-session user-facing fixes (#2253)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#2143): de-flake the G10 windsurf ReDoS-guard wall-clock assertion
The G10 test asserted `elapsedMs < 1000` for a 200k-char payload — a wall-clock
assertion (CLAUDE.md: never assert on wall-clock time) that flaked on a loaded
node24 bench at ~1.1s. It was redundant: runHook's spawnSync `timeout: 10000`
already SIGKILLs a catastrophic-backtracking hook, so the exit-0 assertion is the
real ReDoS guard. Removed the timing assertion; kept exit-0 + documented the
subprocess-timeout mechanism. Surfaced (not caused) by this branch's gsd-test
runs loading the bench; unrelated to the markdown-parsing changes but fixed in
place per the no-flaky-tests rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#2143): fail-loud Result + per-surface write-set contract — Phase 3
Phase 3 of epic #2143 (ADR-2143 §5/§6). The three target bugs (#2140, #2112,
#2118) were already fixed tactically on next; this introduces the reusable
structural contracts and rewires the primary #2140 site onto them.
- src/write-set.cts (new): the parse `Result<T> = {ok,value|reason}` (§5) and the
per-surface write-set (`WriteOutcome {surface, applied, requirement?}`,
`WriteSet`, `writeSetComplete`) (§6). markdown-table.cts now imports + re-exports
`Result` from here (single source; distinct from command-routing-hub's Result).
- requirements mark-complete (src/milestone.cts): returns a PER-REQUIREMENT,
per-surface write-set; `write_set_complete` is true only if every surface of
every requirement applied — structurally forbidding the #2140 OR-into-one-flag
masking, including across a multi-ID batch (adversarial-review regression).
Pre-existing output fields unchanged (behaviour-preserving; #2140 already fixed).
- deriveProgressFromRoadmap (src/phase-lifecycle.cts): removed the vestigial
null-swallowing try/catch (findTableWithColumns never throws) — ADR §5 no-swallow;
RoadmapProgress return contract unchanged.
- commit --files (#2112) and milestone complete --dry-run (#2118) left as-is
(single-surface commit / pre-mutation preview — not genuine multi-surface writes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#2244): backfill changeset PR number (#2251)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#2143): bounded-mutation seam (withSection/withPhaseSection) + phase.cts migration — Phase 2
Phase 2 of epic #2143 (ADR-2143 §4): add a bounded-mutation primitive so a
per-phase ROADMAP edit is structurally confined to that phase's own section,
and migrate the phase-scoped mutation sites in `phase.cts` onto it.
- `src/markdown-sectionizer.cts`: `withSection(content, target, edit, opts?)` —
resolves a section via `collectSection` and applies `edit` to ONLY that
section's body, re-serialising via `replaceSection`. The edit callback sees
only the section body, so any regex it runs is physically confined.
- `src/roadmap-parser.cts`: `withPhaseSection(content, phaseId, edit)` —
resolves a phase's `### Phase N` detail-section heading via the #2121
phase-id source and delegates to `withSection`. Heading match is anchored to
the heading start (a sibling phase whose title mentions the number is not
hijacked) and bounds at the next ATX heading of any level (`levelBounded:false`).
- `src/phase.cts`: `mutateMilestonePhase`'s plan-count and per-plan-checkbox
writes now route through `withPhaseSection` — structurally retiring the
#2130 / #2067 / #2080 boundary-crossing class for these sites. The phase-LIST
checkbox is intentionally left milestone-slice-scoped (it lives outside any
`### Phase N` detail section). Cross-phase renumbering is untouched.
- Property test (fast-check): editing phase k leaves every sibling section
byte-identical; regression tests for title-collision + mixed heading depth.
Behaviour-preserving (verified by old-vs-new differential runs on real
fixtures). Extend-never-mutate (ADR-2143 §2). Registration: CONTEXT.md +
docs/INVENTORY.md export lists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#2243): backfill changeset PR number (#2250)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#2143): markdown table model + schema registry + fail-loud pilot — Phase 1
Phase 1 of epic #2143 (ADR-2143): consolidate markdown table parsing onto a
canonical seam and migrate the pilot reader.
- Add src/markdown-table.cts: parseMarkdownTable (GFM tables -> typed
{columns, rows} addressed by column NAME; ragged rows are typed parse
errors, not silent), a single-source TABLE_SCHEMAS registry
(RoadmapProgress / RequirementsTraceability / QuickTasks / Security, with
variants under one id), matchTableSchema, and findTableBySchema. Result<T>
is scoped to this seam (distinct from the dispatch Result).
- Migrate deriveProgressFromRoadmap (src/phase-lifecycle.cts) off the
position-anchored regex to name-based resolution via the seam — fixes#2137
(the 5-column milestone-grouped Progress table previously returned all-null).
- Add a schema-backed `gsd-tools quick-tasks-append` subcommand and route
fast.md's log_to_state through it, retiring the inline `awk NF-2` column
arithmetic — fixes#2133 (addresses #2012, #2119). Cell values are escaped
(| and newlines) and the STATE.md read-modify-write is atomic under
readModifyWriteStateMd (lost-update race, cf. #500/#905/#1230).
- Writer/reader/template parity test guards TABLE_SCHEMAS against drift
(ADR-2143 §3 Generative-Fix-Divergence).
Registration: .gitignore, eslint.config.mjs, docs/INVENTORY.md +
INVENTORY-MANIFEST.json, CONTEXT.md glossary, docs/CLI-TOOLS.md.
Behaviour-preserving for the canonical 4-column Progress table; the named
bugs are driven fail-first. Extend-never-mutate (ADR-2143 §2).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#2242): backfill changeset PR number (#2248)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2242): escape backslash before pipe in markdown-table cell escaping
CodeQL js/incomplete-sanitization (high): escapeCell escaped | -> \| but not
the backslash itself. Now escapes \ -> \\ before | -> \|, and splitTableRow
unescapes both \\ -> \ and \| -> | symmetrically so cell values (incl.
literal backslashes) round-trip exactly. Added backslash round-trip tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2242): read ROADMAP Progress table by column name — supersede #2168 ad-hoc scan
Rebase reconciliation with #2168 (the tactical #2137 fix that marked itself
"pending #2143"). deriveProgressFromRoadmap now resolves the Progress table via
a new seam helper findTableWithColumns (first table whose header is a superset of
Phase/Plans Complete/Status/Completed, any order, extra columns ignored) and reads
cells by NAME — order/injection-invariant per ADR-2143 §3 — instead of the exact
TABLE_SCHEMAS match. This satisfies #2168's column-invariance property test while
staying seam-based and preserving its `## Progress` scoping (#2012/#1445).
Ragged Progress tables now resolve to null (ADR-2143 fail-loud); updated the stale
state.test.cjs assertion that predated the Phase-1 migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2176): ground the Antigravity reviewer in the repo under review
- capability-probe --add-dir (mirrors the Codex bypass-flag probe) and pass
the repo root on both invocation arms
- anchor _AGY_PROMPT to the absolute repo root; mandate a
REVIEWED-WITHOUT-REPO-ACCESS self-report when the repo is unreadable
- stamp a [reviewed-without-repo-access] marker on self-reported or
scratch-anchored output; Consensus Summary down-weights marked reviews
- apply the same absolute-root anchor to the cursor-agent prompt (AC5)
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* docs(#2176): changeset fragment for PR #2184
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* fix(#2176): review fixes — size baseline, cursor root anchor, anchored blind tells
- regenerate tests/workflow-size-baseline.json for review.md's growth
- cursor anchor uses git rev-parse --show-toplevel (bare pwd resolved the
wrong root from a repo subdirectory)
- blind-review tells anchored: self-report to the first lines of output,
scratch tell to a workspace-declaration phrasing — a grounded review
quoting either string is no longer mis-stamped
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* fix(#2176): round-2 review fixes — scratch-tell bridge, behavioral test, changeset
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* test: regenerate golden-install-parity fixtures for the review.md change
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* test(#2176): pass the transcript path to bash with forward slashes
The behavioral detection test substitutes a mkdtemp path into the bash
compound; on Windows runners that path contains backslashes, which bash
strips, so the transcript is never found and the first assertion fails
(windows-latest/24 lane). Git Bash accepts D:/-style paths.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* fix(#2176): use /gsd:review namespace syntax in workflow comment
The slash-command namespace invariant (#3443) bans retired /gsd-<cmd>
references in Claude-facing sources; a cursor-anchor comment used
/gsd-review. Size baseline + golden fixtures regenerated for the byte
change.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* test(#2176): derive the POSIX path via path.sep, not a hardcoded separator
Review finding: out.replaceAll('\\', '/') hardcodes both separators;
use the separator-safe out.split(path.sep).join(path.posix.sep) idiom.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* test(#2176): use the merged toPosixPath seam for the bash path
Per maintainer note: #2247's shell-command-projection now centralizes
running-OS → POSIX path conversion; import it instead of the inline
split/join idiom.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
Replace every open-coded separator translation across the installer/hooks
source with named, tested seams in shell-command-projection.cts (the platform
seam), removing all hardcoded `/`+`\` from path handling:
- toPosixPath(p) — this machine's native path → POSIX (running-OS relative;
for local filesystem paths).
- toNativePath(p) — POSIX → native (collapses the win32 `/\//g,'\\'` ternary).
- posixNormalize(p)— unconditional `\`→`/`, OS-independent; for emitting paths
to a POSIX/bash TARGET (which may differ from the running
OS) and for parsing mixed-separator input.
core-utils.toPosixPath now delegates to the seam, so its 20+ existing consumers
resolve to one implementation; no duplicate helper.
- ~47 sites across runtime-hooks-surface, runtime-artifact-conversion,
runtime-artifact-install-plan, drift, init, worktree-safety,
installer-migrations, installer-migration-authoring, install-engine, surface,
verify, runtime-artifact-layout, schema-detect, check-command-router.
- Closes the latent POSIX-literal-backslash corruption class (the regex form
corrupts a POSIX path containing a literal backslash; split(path.sep) does not).
- New unit + fast-check property tests for all three helpers.
Closes#2246
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The windows-robustness guard (bug #685) requires every external-binary
spawn to set windowsHide:true so no console window flashes on Windows.
Golden fixtures regenerated for the hook byte change.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* fix(execute-phase): honor gate="blocking-human" in auto-mode checkpoint handling
The package-legitimacy gate (#2827) spans two layers. gsd-executor refuses to
auto-approve a gate="blocking-human" checkpoint and escalates it so a human can
vet the package. execute-phase's checkpoint_handling step then dispatched purely
on checkpoint *type* and never read gate -- so under --auto/--chain it
auto-approved the checkpoint the executor had just refused to auto-approve.
Net effect: the slopsquatting defence was inert in exactly the unattended mode
where it matters. An [ASSUMED]/[SUS] package reached install with no human ever
seeing the prompt.
- gsd-core/workflows/execute-phase.md: carve out gate="blocking-human" (and the
package-legitimacy what-built markers) ahead of every auto-mode branch.
- gsd-core/references/checkpoints.md: document the gate attribute and its two
values. blocking-human previously appeared nowhere outside gsd-executor.md,
so no planner had a documented way to author a non-auto-approvable checkpoint.
- tests/package-legitimacy-gate.test.cjs: the existing regression test asserted
the executor half only, which is why it stayed green while the gate was open.
Now asserts the orchestrator half too.
* chore(changeset): link to issue #2107
* chore(changeset): backfill PR number 2113
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNR8m2pv5U7ubn4iiXVrMa
* test(#2107): refresh golden-install-parity hashes for edited gsd-core files
The golden fixtures pin content hashes for gsd-core/references/checkpoints.md
and gsd-core/workflows/execute-phase.md, both edited by this fix. Regenerated
via UPDATE_GOLDEN=1; only those two keys change across all 17 runtime fixtures.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNR8m2pv5U7ubn4iiXVrMa
* fix(#2107): keep the carve-out inside the ADR-857 host-loop budget
The ADR-857 phase-6 ratchet pins execute-phase.md below 93600 LF bytes so
optional-feature logic keeps migrating out of the host loop. The carve-out
first landed 623 bytes over that ceiling.
Move the two-layer rationale (why gsd-executor escalates these checkpoints)
into references/checkpoints.md, where the gate is now documented, and reduce
the workflow to the operative rule. execute-phase.md is 93589 bytes, under
the ceiling; the gate token and both <what-built> marker strings are kept
because the orchestrator matches on them.
Refresh the two baselines the edit invalidates: golden-install-parity
fixtures (only the checkpoints.md and execute-phase.md hashes move) and
workflow-size-baseline.json (one line). The ADR-857 ceiling itself is
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JNR8m2pv5U7ubn4iiXVrMa
* fix(#2107): executor honors blocking-human on the decision branch + gate transport
Review found the fix incomplete one layer down. Two executor-layer gaps:
1. Blocker — agents/gsd-executor.md auto-mode dispatch gated
checkpoint:human-verify on gate="blocking-human" but the checkpoint:decision
branch below auto-selected the first option with no gate check. The executor
resolves a decision itself (auto-selects and continues) without returning it,
so the orchestrator carve-out never runs for it. A planner following the new
checkpoints.md rule 6 ("gate a decision whose default would be wrong to
assume") would have it silently auto-selected under --auto/--chain — the exact
#2107 harm, one checkpoint type over. The decision branch now STOPs and
returns for an explicit human decision when gate="blocking-human".
2. Major (transport) — checkpoint_return_format carried no field conveying the
gate to the freshly-spawned orchestrator, so recognition of the proactive
pre-install checkpoint rested on freeform prose. Added a **Gate:** field to
the return format and re-pointed the execute-phase carve-out at it
("If the returned Gate: is blocking-human"). Net byte-negative: execute-phase.md
drops 93589 -> 93583, widening ADR-857 headroom from 11 to 17 bytes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#2107): cover decision carve-out + gate transport, de-vacuum conditional tests
- New: 'auto mode does not auto-select a blocking-human decision checkpoint'
asserts the executor decision branch STOPs on blocking-human. Verified red on
the pre-fix executor (2 fail), green with the fix (27 pass).
- New: 'checkpoint_return_format transports the gate ...' asserts the **Gate:**
field carries blocking-human across the executor->orchestrator boundary.
- New: 'auto-select rule for decision is conditional' — orchestrator-side mirror
of the human-verify conditional test, for the execute-phase decision branch.
- Fix vacuous test: both conditional tests now assert the anchor matched
(length > 0) before iterating, so anchor drift can no longer pass with zero
assertions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(#2107): refresh golden + size baselines for executor + execute-phase edits
Regenerated via UPDATE_GOLDEN=1 and update-size-baseline.cjs. Only the
gsd-executor.md and gsd-core/workflows/execute-phase.md hashes move across the
runtime fixtures (35 ins / 35 del, no keys added or removed); checkpoints.md is
unchanged this round. Size baselines: gsd-executor.md 43607 -> 43973,
execute-phase.md 93589 -> 93583 (still under the ADR-857 ceiling).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
- readGitStatus calls execFileSync via the child_process namespace so tests
can inject spawn failures through the shared module object
- two deterministic tests: ERR_CHILD_PROCESS_STDOUT_MAXBUFFER-shaped and
ETIMEDOUT-shaped throws both degrade to null (segment absent), proving
the fail-soft paths the PR previously only asserted
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
- changeset reformatted to the bold-lead + (#2163) house convention
- explicit 8 MiB maxBuffer on the git spawn (default 1 MiB could overflow on
huge dirty repos; overflow still degrades to segment-absent)
- child_process require hoisted to module level per file style
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
New statusline.show_git config (default false). When enabled, a git
segment renders after the directory: current branch plus compact
work-state markers (+staged ~unstaged ?untracked ↑ahead ↓behind, or ✓
when clean and in sync), e.g. " │ main+2~1?3".
One git status --porcelain=v2 --branch spawn per render via execFileSync
with a fixed argument array (no shell), a 1.5s timeout, and the
workspace dir passed with -C. Fails silently — segment absent outside a
repo, without git, or on timeout. Default output is unchanged when the
flag is absent.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* feat(#2161): opt-in absolute token count on the statusline context meter
New statusline.show_context_tokens config (default false). When enabled,
the context meter shows the absolute token total after the percentage,
e.g. "████░░░░░░ 46% (156k)" — summing input, cache-creation, cache-read,
and output tokens from context_window.current_usage (matching /context).
Default output is byte-for-byte unchanged when the flag is absent or
false. The .planning config is now read once per render and shared with
the last-command/position block instead of being re-read.
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* docs(#2161): changeset fragment for PR #2174
* fix(#2161): review fixes — k-to-M threshold, boundary tests, changeset format
- formatTokens promotes to the M branch when k-rounding reaches 1000
(999,500-999,999 rendered "1000k" instead of "1.0M")
- boundary tests at 999499/999500/999999/1000000/1000001
- Number() guards on the four usage fields (silent string-concat gap)
- changeset body ends with the (#2161) citation per house convention
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* fix(#2161): round-2 review fixes — config-set coverage, precision claim, exports style
- config-set accept/reject tests for statusline.show_context_tokens
(mirrors the post-planning-gaps precedent the issue scope names)
- changeset + docs no longer claim parity with /context: the suffix sums
four fields while the meter %% derives from used_percentage (three), so
the figures can diverge slightly
- module.exports one entry per line
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
* test: regenerate golden-install-parity fixtures for the statusline hook change
Claude-Session: https://claude.ai/code/session_01Hme55Pvq6BhpgwBcyC5HAg
---------
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
* fix(#2206): strip trailing slashes in isGitIgnored to avoid CRLF check-ignore quirk
isGitIgnored was called with a trailing slash (`.planning/`) in config-loader.
git check-ignore has a longstanding quirk: a CRLF .gitignore with blank lines
falsely reports any path WITH a trailing slash as ignored. This silently set
commit_docs=false on Windows repos (where CRLF .gitignore is the norm), skipping
all planning-doc commits. Normalize trailing slashes inside isGitIgnored so every
call site is protected.
Closes#2206
* docs(#2206): add changeset fragment
* docs(#2206): backfill PR number
* fix(#2203): traceability parser matches REQ-IDs in any column, not just the first
The traceability table-row parser required the REQ-ID in the first column
(`^| REQ-ID |`). A table that leads with a status column (e.g. `| ☐ | REQ-01 |`) matched zero rows, so phase complete warned every body REQ-ID was missing.
Match REQ-IDs in any pipe-delimited cell (drop the ^ anchor).
Closes#2203
* docs(#2203): add changeset fragment
* docs(#2203): backfill PR number
* fix(#2202): preserve unknown frontmatter keys in syncStateFrontmatter
syncStateFrontmatter rebuilds frontmatter from a fixed schema, dropping any
custom/unknown key on every mutating verb. Before reconstruction, merge any
existing frontmatter key the schema does not own. Schema keys still win.
Closes#2202
* docs(#2202): add changeset fragment
* docs(#2202): backfill PR number
* fix(#2202): add regression test + remove redundant type assertion
- tests/state.test.cjs: behavioral regression test asserting custom/unknown
STATE.md frontmatter keys survive a mutating verb (they were silently dropped
before the syncStateFrontmatter carry-forward).
- src/state.cts: drop the unnecessary `as Record<string, unknown>` assertion
that tripped @typescript-eslint/no-unnecessary-type-assertion (the lint-tests
gate failure).
Refs #2202
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The <execution_context> reference presented @~/.claude/gsd-core/... as canonical
and described the paths as files "the executor reads before starting". Both are
misleading: the prefix is install- and runtime-relative (Claude global vs Cursor
.cursor/gsd-core vs an absolute --local path), so a committed plan is not
clone-portable, and /gsd-execute-phase loads the workflow from its own installed
copy rather than gating on the committed block.
- docs/reference/plan-md.md: describe the install-relative, non-clone-portable
nature of the block and contrast it with repository-relative <context>.
- .out-of-scope/plan-md-execution-context-portability.md: record the #2238
wontfix decision (PLAN.md is a machine artifact; #2158 precedent) with a
revisit-if condition.
Refs #2238. Closes#2240.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "prove the defect first" artifact the #612 approval requires. Green
current-behavior characterization (zero behavior change) asserting that the
milestone-prefixed grammar collapses a fully-specified 4-tuple token to a bare
integer:
normalizePhaseName('2-01.02-01') === '02' (the anchor)
normalizePhaseName('10-02.03-04') === '10' (same failure class)
plus the defect boundary (collapse appears only when milestone+phase+subphase+
plan coexist) and the cross-subsystem '02-04' semantic ambiguity (asserted as a
non-collapsing current output, a distinct class from the parse collapse).
Behavioral only, no source-grep. Locks the current known-imperfect behavior as
documentation of the ambiguity; to be superseded when PR-1 lands the bracket
grammar behind phase_id_convention: 'bracket'. Named adr-612-* (maps to no prod
module) so it clears the lint-test-file-count 2-file cap on the phase-id module
and the lint-regression-test-names bug-* ban.
See docs/adr/612-bracket-phase-id-convention.md, Decision 3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR-0 of the #612 tracer-bullet sequence: the ADR that locks the contract
PR-1..PR-6 execute against. No production code.
Rewrites the design for current next (v1.7.0-rc.5): phase-id surfaces now
live in src/phase-id.cts under the ADR-2121 single-owner regime (core.cts
retired, #1267), and M-NN is a shipped first-class convention rather than a
no-adopter RC intermediate. States every blocking requirement from the
approved-enhancement comment as an explicit design commitment:
- terminal M-NN deprecation, end state two conventions (null + bracket) by
forward consolidation via the migrator, not "no adopters"
- EMIT/RENDER as one pure pair with fast-check round-trip properties, inside
phase-id.cts under the #2128 token-source / drift-lint regime
- single-sourced, generated PR-6 injection block + verify parity check
- migrator dry-run-default / dirty-tree guard / atomic rollback (the current
base's surgical reverse-rename #1542, a deliberate improvement over the
requirement's literal "HEAD-sha reset" — flagged) + two-invariant fixture
corpus + M-NN lift + HARD-REFUSE on absent project_code
- everything gated on phase_id_convention === 'bracket'; null / M-NN paths
byte-untouched (never gate on project_code)
- concrete collision anchor normalizePhaseName('2-01.02-01') === '02',
grounded in the live regexes at src/phase-id.cts:71/79
Guardrail: bracket is core config-gated behavior, not an add-only capability.
Per-PR implementation map (PR-1..6) targets current module homes and the
CARRY-FORWARD ledger. Adds the docs/adr/README.md index row.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address @trek-e's review (Option 2, interim tactical fix) on PR #2168:
- Add a fast-check property test (tests/derive-progress.property.test.cjs)
asserting deriveProgressFromRoadmap's derived counts are invariant to
header column ORDER and COUNT — shuffle the header cells and inject
unrelated columns, assert counts match the data. This pins the parser's
core new capability (header-name lookup vs. the old position-locked regex).
Named derive-progress (not phase-lifecycle) so it isn't greedily attributed
to the shorter `phase` prod prefix under lint-test-file-count; like the other
*.property.test.cjs files it maps to no module and is cap-exempt.
- Add boundary regression tests in state.test.cjs: header+separator only
(0 rows -> all-null), exactly 1 row, and ragged rows (extra/short cells,
handled without throwing).
- Mark the adhoc Progress-table scan as pending the ADR-2143
parseMarkdownTable/TABLE_SCHEMAS seam (re-point pending #1372 -> #2143).
- Fix the changeset lead-in to the required bold form.
Per the maintainer, reader/writer parity and CONTEXT.md updates are
explicitly out of scope for this stopgap — Phase 1 (ADR-2143) owns them.
Validated: property test 200 runs green; state.test.cjs #2137 block 8/8;
eslint clean; lint-test-file-count green; full `npm test` exit 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(2119): single SECURITY.md writer — auditor is return-only
The gsd-security-auditor held Write/Edit and was instructed to write
SECURITY.md (no <N>- prefix, no template frontmatter), while the
orchestrator's Step 6 also wrote the correct padded <N>-SECURITY.md
from templates/SECURITY.md. Two writers, two naming conventions, two
shapes — the auditor's unprefixed file was invisible to the workflow's
*-SECURITY.md glob detector and unparseable for the threats_open gate.
Fix (option 1 from the issue): make the auditor return-only.
- Remove Write/Edit from auditor's tools
- Rewrite all 'Write SECURITY.md' instructions to 'Return structured
verdict' with threats_open count
- Add explicit constraint in workflow Step 5 spawn prompt
- Update existing test (was asserting Write in tools — now asserts absence)
- Add new regression test for single-writer contract
- Update docs/AGENTS.md stale Tools/Produces rows
- Regenerate golden fixtures + agent size baseline
* docs(changeset): backfill PR number (#2154)
* chore(#2119): regenerate pi/qwen golden fixtures after next merge
The single-writer change edits gsd-core/workflows/secure-phase.md and
agents/gsd-security-auditor.md; pi.json (added on next) and qwen.json (merge
straggler) were the only runtime fixtures still holding pre-change hashes for
those files. All other runtimes already reflect the change. Regenerated via
the sanctioned gen-golden-install-parity script.
* merge origin/next — regenerate goldens + baseline for merged state
* fix slash-command syntax: /gsd-secure-phase → /gsd:secure-phase (#2154 CI fix)
* fix(#2200): scope phase-complete roadmap writes to the current milestone
runPhaseCompleteTransaction's roadmap mutators ran unanchored / un-milestone-
scoped / first-match over the whole ROADMAP: the phase-checkbox flip could check
a bullet inside a backticked prose literal or an earlier Backlog entry instead of
the closing phase's bullet (a transposition), and the Plans-count writer could
bind to a same-numbered phase in a shipped milestone.
- Add currentMilestoneRawRanges(content, cwd) to roadmap-parser.cts: raw
[start,end) offsets of the active milestone's region(s) (primary section + the
optional Phase Details section), mirroring extractCurrentMilestone's selection.
- Line-anchor the phase checkbox pattern (^ + m flag) so an inline / backticked
prose literal cannot match.
- Apply the phase-checkbox flip, the Plans-count write, and the per-plan checkbox
flips ONLY within the current milestone window(s) via a mutateMilestonePhase
helper (splice later windows first so offsets stay stable). Fall back to whole-
content mutation when there is no versioned active milestone (prior behaviour).
The Progress-table writer stays as-is (already scoped to ## Progress, #2012).
Closes#2200
* docs(#2200): add changeset fragment
* docs(#2200): backfill PR number in changeset fragment
* fix(2112): scope commit to --files pathspec, not entire index
cmdCommit/cmdCommitToSubrepo/cmdPrSubrepo staged exactly the files
named in --files but then ran a bare 'git commit' with no pathspec,
absorbing anything else in the index into a commit whose message
described only the named files (#2112).
Fix: append '-- ...stagedPaths' to the commit args when the caller
declared a scope. Three guards are load-bearing:
- stagedPaths (not filesToStage) excludes skipped missing files (#2014)
- explicitFiles gate keeps the default .planning/ path byte-identical
- MERGE_HEAD check via 'git rev-parse' falls back to bare commit during merge
- --amend is left without pathspec (different operation)
cmdPrSubrepo pathspec uses changedFiles (old+new for renames) so the
full rename is captured atomically.
Also fixes workflow markdown in spec-phase.md and add-tests.md.
All-files-missing now short-circuits to nothing_to_commit instead of
absorbing the entire index under a message describing files that
were not committed.
* docs(changeset): backfill PR number (#2148)
* test: update golden-install-parity fixtures for workflow markdown changes (#2112)
* test: update golden fixtures + workflow baselines for #2112 changes
- claude-local.json golden fixture (now generated via gen script)
- workflow-size-baseline.json (add-tests.md +16, spec-phase.md +42 bytes)
- Extended gen-golden-install-parity-zcode.cjs to also regenerate the
claude local-layout fixture
* fix(2118): honor --dry-run in milestone complete with zero-mutation preview
milestone complete treated --dry-run as a no-op: the flag was neither
parsed nor rejected, so a caller who expected a preview instead
triggered the full destructive mutation (archive phases → move audit
artifacts → rewrite STATE.md) with no way to back out.
Fix (option 2 from the issue): add dryRun to MilestoneCompleteOptions,
parse --dry-run in the dispatcher, and return a JSON preview plan
(would_archive, would_update) after the read-only stats gathering but
before any mutations. Also gated platformEnsureDir on !dryRun so the
archive directory is not created during preview.
3 regression tests: no-mutation happy path, --no-archive-phases combo,
and --force bypass combo.
* docs(changeset): backfill PR number (#2155)
* chore(#2118): regenerate pi/qwen golden fixtures after next merge
The milestone --dry-run fix changes gsd-core/bin/gsd-tools.cjs; qwen.json
(missed at authoring) and pi.json (added on next, never carried the fix)
were the only two runtime fixtures still holding the pre-fix hash. All
other runtimes already reflect the change. Regenerated via the sanctioned
gen-golden-install-parity script.
* fix(#2118): surface accomplishments in dry-run preview; fix --dry-run --raw
Orthogonal review findings on the --dry-run preview:
- The preview omitted the already-computed accomplishments (the primary
MILESTONES.md content a real run writes); surface it as a top-level field,
mirroring the real-run result.
- `--dry-run --raw` discarded the structured payload and printed the literal
string "dry-run"; drop the raw-value arg so --raw emits the full preview
JSON, matching the real-run output() call.
Adds tests: --dry-run --raw is parseable JSON, preview includes accomplishments,
and --dry-run --force is proven zero-mutation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#2201): accept --phase N flag in the phase verb family (complete, list-plans)
The phase family router treated the first positional (args[2]) as the phase
number, so `phase complete --phase 12` passed the literal '--phase' as the phase
→ 'Phase --phase not found'. The state family already accepted --phase N. Now
complete and list-plans accept --phase N (and --phase=N) as well as the bare
positional; unrecognized flags yield a usage error naming the accepted form.
Closes#2201
* docs(#2201): add changeset fragment
* docs(#2201): backfill PR number
* test(#2199): cover bullet/em-dash ROADMAP phase resolution + milestone count
Adds the bullet-only ROADMAP fixture the suite lacked: an all-bullet em-dash
ROADMAP resolves each phase (no Phase null), colon/en-dash/hyphen bullet
separators all resolve, mixed heading + bullet forms coexist, and the milestone
phase-count counts bullet-form phases instead of collapsing to zero.
* fix(#2199): accept bullet/em-dash phase entries in roadmap lookup + milestone filter
Roadmap phase lookup (findRoadmapPhaseInContent) matched only ATX headings
against a colon-required pattern, so a bullet/checkbox entry like
`- [ ] **Phase N — name**` — which the bundled roadmapper emits in bullet-house-
style ROADMAPs — resolved found:false and `Phase null` was written into STATE.md.
The milestone phase-filter built its phase set from headings only, so a bullet-
only ROADMAP collapsed to a zero-count pass-all filter and progress denominators
broke.
- Add a shared bullet-phase-line pattern (separator: em-dash/en-dash/hyphen/colon).
- findRoadmapPhaseInContent: on a heading-match miss, fall back to the bullet line
for the requested phase; return found:true with the captured name.
- getMilestonePhaseFilter: also scan bullet lines into the milestone phase set.
Closes#2199
* docs(#2199): add changeset fragment
* fix(#2199): bullet phase lookup as last resort + consolidate test (review)
Two corrections to the initial fix:
1. Regression — the bullet fallback inside findRoadmapPhaseInContent was too
eager: it returned a bullet match from the scoped (current-milestone) content
before the caller tried the full-content heading path, so a phase whose
Requirements live in a Phase Details heading (after the active-milestone
section) got a bullet-line section with no Requirements → phase_req_ids null
(broke 3 init tests). Restructure: findRoadmapPhaseInContent is heading-only
again; a separate findRoadmapBulletPhaseInContent runs in getRoadmapPhaseInternal
ONLY after scoped + full heading lookup fails, so a heading with a Requirements
section always wins.
2. lint-test-file-count — the standalone fix-2199-roadmap-bullet-phase.test.cjs
collided with the over-cap 'roadmap' module (FAIL_NOVEL_FILES). Consolidate
the regression into the existing tests/roadmap-parser.test.cjs (its natural
home, under the 2-file cap).
* test(#2199): assert heading-in-full beats bullet-in-scoped (review L3)
The exact first-attempt regression: a phase has a bullet in the active-milestone
scope but its heading (carrying Requirements) lives in a Phase Details section
outside that scope. Pin that the heading section wins over the bullet line so
req_ids resolve and the eager-bullet bug cannot return.
* docs(#2199): backfill PR number in changeset fragment