2fe17cb87df4901aa4093ff89323500a84a072ac
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2fe17cb87d |
fix(core): writeStateMd must scan inside the lock (M8)
Root cause: writeStateMd computed its frontmatter disk scan (syncStateFrontmatter — the READ half of a read-modify-write) BEFORE acquireStateLock, leaving a TOCTOU window. A concurrent writer that committed a new PLAN/SUMMARY between our scan and our lock made writeStateMd stamp stale progress counts (lost update — the #500/#905/#1230 family). The atomic sibling readModifyWriteStateMd already scans inside its lock. Fix: move _diskScanCache.delete + syncStateFrontmatter inside the acquireStateLock-held try, before platformWriteSync. Byte-for-behaviour identical for single-threaded callers — only the concurrent-writer window closes. Adds an afterAcquire test seam (mirrors the M1 _setLockProbes seam) to make the window deterministic; new test proves RED (stale count) before the reorder and GREEN after. Source of truth src/state.cts (ADR-457); bin/lib/state.cjs is generated. Claude-Session: https://claude.ai/code/session_01R88n7Q54bAaVHFkDbbH1yz |
||
|
|
fc9bd70aff |
fix: PID-liveness gate for the two core-path file locks (audit M1+M2)
The STATE.md write lock (acquireStateLock) and the .planning/ workspace lock (withPlanningLock) stole contended locks on mtime age alone with no process.kill(pid,0) liveness check, and mis-ordered stale-vs-wait so a live-but-slow holder could be robbed mid-critical-section. M1 (lost update / STATE.md corruption): a live writer whose critical section ran past the stale threshold aged out and a waiter unlinked its lock and acquired -> two writers in STATE.md's read-modify-write window. mtime is a leaky proxy for "holder is alive"; it leaks under exactly the slow-holder condition the lock guards against. M2 (uncaught EEXIST): withPlanningLock's timeout fallback unconditionally unlinked whatever lock existed (even a live holder's) and re-acquired OUTSIDE any try -- a concurrent re-create raced a raw EEXIST out of the helper. Fix backports capability-lock.cts's liveness gate (process.kill(pid,0) via a _setLockProbes/_resetLockProbes test seam): - acquireStateLock: steal when holder pid is DEAD (any age) OR age exceeds a deadman ceiling (60000ms, ABOVE maxWaitMs=30000) so a verified-live holder is never stolen within budget; garbage/legacy bodies stay recoverable. - withPlanningLock: same gate in the EEXIST path (dead stolen promptly, live waited on); removed the unconditional force-steal -> clear timeout throw, which also closes M2 (no re-acquire outside try). Uncontended path unchanged byte-for-behaviour; realClock + real process.kill remain the defaults. Pid-reuse residual fails safe (waits/times out, never corrupts) and recovers at the deadman ceiling. Tests: TDD red->green via the clock + new pid-liveness probe seams (no wall-clock; #453 deleted the race tests). 8 new behavioural tests across tests/clock-seam.test.cjs and tests/planning-workspace.test.cjs; the prior withPlanningLock timeout test rewritten to pin the no-force-steal contract. Claude-Session: https://claude.ai/code/session_01R88n7Q54bAaVHFkDbbH1yz |
||
|
|
4719b36d41 |
fix(#1445,#1446): exclude 999.x backlog from milestone totals; allow total_phases downward correction (#1490)
* fix(#1445,#1446): exclude 999.x backlog phases from milestone totals; allow total_phases downward correction #1445: deriveProgressFromRoadmap (phase-lifecycle.cts), the roadmapPhaseCount loop (state.cts), and getMilestonePhaseFilter (roadmap-parser.cts) all now skip phase tokens matching /^999\b/ — consistent with the existing init.cts filter. 999.x backlog dirs are consequently excluded from phaseDirs too. #1446: shouldPreserveExistingProgress (state-document.cts) no longer includes total_phases in its ratchet check. total_phases always takes the freshly derived value; only completed_phases, total_plans, and completed_plans retain ratchet behaviour. Regression tests added for both bugs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add changeset for #1445/#1446 progress-backlog-exclusion-and-ratchet Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#1445,#1446): rename test files to fix-NNN convention; fix changeset pr: null Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
666d933e16 |
ci(#1401): add no-adhoc-markdown-parsing rule (fence-strip + section-collect) + grandfather burn-down (#1402)
Tightens the over-broad heading-walk detection: removes heading-walk entirely and narrows fence-regex to require a multiline body ([\s\S]), so single-line tests like /^```/ and /^###\s+/ are no longer flagged. Grandfathers the 10 genuine section-collect sites across state.cts, milestone.cts, audit.cts, and phase-lifecycle.cts with concrete reasons. Adds 12 RuleTester tests (3 positive, 9 negative) to eslint-rules.test.cjs. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ce8bcb1b95 |
refactor(#1398): migrate state.cts section-collects onto markdown-sectionizer seam, byte-identical STATE.md (epic #1372 T6) (#1399)
* refactor(#1398): migrate state.cts section-collect regexes onto markdown-sectionizer seam (epic #1372 T6) Replace ≈18 hand-rolled `/(heading)([\s\S]*?)(?=stop)/` regex splices in state.cts with direct `tokenizeHeadings` calls that compute the exact [bodyStart, stopOffset) span, preserving byte-identical STATE.md output. Non-migratable site left in place: `cmdStateRecordMetric`'s metricsPattern captures table-header rows in group 1 — not a standard heading+body shape. Write orchestration (readModifyWriteStateMd / syncStateFrontmatter / shouldPreserveExistingProgress / #952 no-op guard) is UNTOUCHED. Verification: t6-headtohead.cjs head-to-head harness runs 25 ops across 6 STATE.md fixture variants (inline, trailing-blanks, CRLF, no-frontmatter, nested-acc, post-milestone) against origin/next and reports 0 diffs. No-op guard confirmed: record-session on recorded:false leaves STATE.md byte-identical. All 150 state tests and 62 milestone/forensics tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(#1398): capture T6 state-section-splice characterization; drop throwaway harness Remove scripts/t6-headtohead.cjs (committed throwaway HEAD-vs-origin/next byte-compare harness). Capture its coverage as 23 behavioral characterization tests appended to tests/state.test.cjs, exercising all migrated cmdState* write-ops across 7 fixture variants (inline, trailing-blanks, CRLF, no-frontmatter, nested-acc, no-current-pos, post-milestone). Includes the #952 no-op guard (recorded:false + byte-unchanged assertion) and CRLF/trailing-blanks edge-case coverage. Also removes the dead spliceStateSection helper (defined but never called) that was surfacing as an @typescript-eslint/no-unused-vars warning. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
c20d741dc9 | fix(#1316): preserve prose STATE phase names (#1351) | ||
|
|
80014109c1 | fix: preserve state patch progress counters (#1345) | ||
|
|
645601a10d |
refactor(#1286): T4 — migrate 5 large destructure callers off the core spine (batch 3) (#1287)
Migrate the entire core surface of commands (~23 symbols), phase (~17), roadmap, state, template to the leaf modules directly (behaviour-identical — leaves are the objects core re-exports by reference). All 5 now import zero core symbols and are removed from the allowlist (9 -> 4). Dropped a dead `void replaceInCurrentMilestone` from phase.cts; stale core.cjs docstrings fixed. core.cts re-exports untouched (serve the remaining 4 idiom-hard files); teardown is T-final. No behaviour change. Closes #1286 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
00c447701d |
fix(#1257): update pipe-table Status/Phase/Plan cells in planned-phase + begin-phase (#1260)
* fix(#1257): update pipe-table Status/Phase/Plan cells in planned-phase + begin-phase cmdStatePlannedPhase ran its body-field replacements on the full file content, so the case-insensitive ^Status: pattern matched the YAML frontmatter `status:` line before the body `| Status | … |` cell — the cell never advanced to 'Ready to execute' and syncStateFrontmatter re-derived the stale 'planning' status (the #1230 delta heuristic preserved it). cmdStateBeginPhase had pipe-table else-branches only for Status/Last activity (#1256), so the Current Position `| Phase |` / `| Plan |` cells were left stale while a spurious inline `Phase: N — EXECUTING` line was prepended. Both handlers now strip frontmatter before body-field replacement and update the pipe-table cells in place via stateReplaceField, matching inline-format behaviour. Systemic residual of #1255 / #1256. Adds 4 regression tests (#1257 block in tests/state.test.cjs) covering both findings — RED before the fix, GREEN after; full state-area suite (437) and local unit suite stay green. Closes #1257 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1257): backfill changeset pr number (#1260) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
2668fbbeb0 |
fix(#1255): advance frontmatter status for pipe-table STATE.md on begin/complete-phase (#1256)
* fix(#1255): advance frontmatter status for pipe-table STATE.md on begin/complete-phase state begin-phase/complete-phase called stateReplaceField on the FULL file content, so its case-insensitive ^Status: plain-pattern matched the YAML frontmatter status: line first (no g flag) and never updated the body pipe-table Status cell; syncStateFrontmatter then re-derived the stale status from the unchanged body, freezing the frontmatter status. Fix: strip frontmatter before the body-field replacements (operate on body only), reassemble with frontmatter preserved, so the body Status cell updates and the frontmatter derives correctly — for inline AND pipe-table body formats. Also corrects the Current Position pipe-table else-branches (Status/Phase/Last-activity) to write bare, consistent cell values. Pipe-table Status is a supported body format (not rewritten to inline). Closes #1255 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1255): add changeset for pipe-table state status fix (#1256) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1255): fold pipe-table regression into state.test.cjs + Windows-portable frontmatter regex Per the 2026-06 audit, new tests/bug-NNNN-*.test.cjs files are banned (lint-regression-test-names) — folded the 7 #1255 regressions into tests/state.test.cjs and removed the standalone file + its lint-test-file-count allowlist entry. Also fixed the frontmatter assertions' /^---\n/ anchors to /^---\r?\n/ (windows-test-parity-guard frontmatterAnchorLiteralNewline). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
6724581285 |
fix(#1230): preserve frontmatter status/stopped_at when a state write doesn't change the body source (#1252)
* fix(#1230): preserve frontmatter status/stopped_at when a state write doesn't change the body source Adds a delta heuristic in `readModifyWriteStateMd`: snapshots the body Status and Stopped At fields before the transform, then after syncStateFrontmatter runs, restores the existing frontmatter values for any field whose body source was not changed by this write. Integrates cleanly with the existing !resync progress-restore block (computes postFm once, applies both restorations, reconstructs frontmatter once). Regression tests added to bug-905 test file (same theme, no new file). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(#1230): add Fixed changeset fragment --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
e97b5d6b7b |
fix(#1217): bound acquireStateLock retries on recoverable errno (no busy-spin) (#1236)
* fix(#1217): bound acquireStateLock retries on recoverable errno (no busy-spin) The recoverable-errno branch (`ACQUIRE_LOCK_RETRY_ERRNOS`) previously called `continue` directly, skipping both `clock.sleep()` and the 30 000 ms budget check. A permanently-failing ENOENT (e.g. parent dir removed) would spin at 100% CPU forever with the event loop fully blocked, making the OS-level `timeout` the only escape. Fix: extract a `checkBudgetAndSleep(context)` helper and call it from BOTH the recoverable-errno path and the EEXIST contention path so every retry is bounded and backed off identically. Regression tests added to tests/clock-seam.test.cjs: - persistent ENOENT throws budget-exceeded error (clock must advance via sleep) - transient ENOENT (2 retries then success) acquires lock normally Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1217): backfill changeset PR number (#1236) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
3827054954 |
fix(#1156): support table-format STATE.md and insert missing roadmap plan rows (#1172)
* fix(#1162): support table-format STATE.md in state field read/replace Extend stateExtractField and stateReplaceField in state-document.cts to detect and operate on pipe-table rows (| Field | value |). The separator row | --- | --- | is excluded from matching. updateCurrentPositionFields in state.cts now falls through to stateReplaceField for table-format Current Position sections when the inline Status:/Last activity: patterns do not match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1163): insert missing plan checklist rows in roadmap update-plan-progress cmdRoadmapUpdatePlanProgress now inserts `- [ ] NN-XX-PLAN.md` checkbox rows under the phase Plans: line when no per-plan checkbox rows exist yet (fresh template). Rows are sorted ascending and any already-summarised plans are immediately marked [x]. The planCountPattern is extended to also match plain `Plans:` (in addition to bold `**Plans:**`) so plan counts are updated in both template variants. The existing-rows check covers both top-level and indented checkbox forms to preserve idempotency. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1163): fill partial plan-row gaps and scope insertion to active milestone - Finding 1 (HIGH): replace all-or-nothing rowsAlreadyPresent guard with per-file set-difference so missing rows are inserted even when SOME plan rows already exist - Finding 2 (MEDIUM): extend planCountPattern to recognise **Plans**: (canonical template form — bold word + outer colon) alongside **Plans:** and plain Plans:; use two-pattern fallback for row insertion to anchor under Plans: checklist header rather than the **Plans**: summary line - Finding 3 (MEDIUM): scope row insertion to the active (post-</details>) milestone region so duplicate phase headings in archived sections never receive new rows - Finding 4 (LOW): rename misleading "pipe-like content" test to honestly describe what it tests (multi-row table isolation); add out-of-scope escaped-pipe comment - Remove now-unused anyCheckboxMatched variable (lint clean) - Add 5 adversarial regression tests (pre-fix failures confirmed) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1163): scope missing-plan detection to active milestone; preserve authored state fields in table format - roadmap.cts: compute activeRegion (post-</details> slice) once and use it for BOTH missingPlans detection and row insertion, so archived <details> rows no longer suppress active-section inserts (Finding 1 code-review) - roadmap.cts: change (Plans:) inner capture to non-capturing (?:Plans:) in insertRowsPatternA to prevent group-numbering shift (Finding 3) - state.cts: mirror inline-branch preserve-authored guards onto table-format branches in updateCurrentPositionFields — Status table branch checks isInList/matchesPattern before replacing; Last Activity table branch checks isDateShape/inList, preserving executor-authored narrative prose (Finding 2 code-review) - tests: add three failing-first regression tests confirming each finding Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1162,#1163): fold table-format + plan-row regressions into owning module tests Move bug-1162 cases into tests/state.test.cjs and bug-1163 cases into tests/roadmap.test.cjs under named regression describe blocks; delete the standalone bug-NNNN files and prune their allowlist entries. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1156): add changeset fragment for table-format state + roadmap insert fix Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
b431b1fab4 |
fix(#1140): implement state add-roadmap-evolution CJS handler (#1148)
* fix(#1140): implement state add-roadmap-evolution CJS handler `query state.add-roadmap-evolution` was unreachable: the CJS state router listed it in the `unsupported` map with a circular message ("...is SDK-only. Use: gsd-tools query state.add-roadmap-evolution ...") and no CJS handler existed after the SDK retirement (ADR-0174). Every `/gsd:phase insert` and `/gsd:phase --edit` run hit a dead end recording Roadmap Evolution. Re-implement `cmdStateAddRoadmapEvolution` in CJS (src/state.cts) and wire it into the state router; remove the now-stale `unsupported` entry. The handler appends a single-line bullet under `## Accumulated Context` → `### Roadmap Evolution` (creating the subsection/section if missing, deduping identical entries), scoping every lookup to the Accumulated Context body so a decoy heading in an unrelated section is never targeted, and flattening multiline notes to a single bullet. Section-boundary regexes mirror the sibling add-decision/add-blocker handlers and preserve following sections on CRLF input. Regression cases live in tests/state.test.cjs (per the no-new-bug-NNNN-files policy) and cover the literal issue repro plus the CLI/parser QA matrix (missing/empty/whitespace note, flag-shaped value, duplicate flags, hostile shell metacharacters, Unicode, decoy section, CRLF, missing STATE.md). Closes #1140 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#1140): backfill changeset PR number (1148) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
e837bfc1de |
fix(#1101): record-session updates ## Session Continuity in place, no duplicate block (#1113)
The reported symptom (recorded:false yet STATE.md frontmatter still mutated) was already resolved on next by #944/#948. This fixes the residual: the DWIM auto-create recognised only the canonical `## Session` heading, so a bootstrap `## Session Continuity` section fell through to the append branch and produced a second `## Session` block. Insert only the missing canonical fields after the `## Session Continuity` heading — preserving the heading and any prose (e.g. "Next recommended action") — and teach the snapshot / frontmatter readers to recognise that heading (the optional ` Continuity` group still excludes `## Session Continuity Archive`, preserving #2444 scoping). Closes #1101 Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
46967baae8 |
fix(#948): guard STATE.md no-op writes; preserve milestone_name/stopped_at (closes #944) (#952)
* test(#948): add regression tests for no-op write guard and record-session auto-create (#944) Red before fix: 11/15 tests fail. Green after: 15/15. Covers zero-match patch byte-identity, milestone_name preservation, stopped_at frontmatter-wins, record-session auto-create fallback, and adversarial fixtures (CRLF, empty body, non-canonical labels). Also registers bug-948-state-noop-write-guard.test.cjs in the state bucket of lint-test-file-count.allowlist.json. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#948): guard STATE.md no-op writes; preserve milestone_name/stopped_at (closes #944) Shared root cause: `readModifyWriteStateMd` wrote STATE.md unconditionally even when the transform produced no change, and `syncStateFrontmatter` re-derived frontmatter from the possibly-stale body on every write. Three coordinated fixes in src/state.cts: 1. readModifyWriteStateMd: add no-op guard — when transform result === input content, skip the write entirely (no platformWriteSync, no last_updated bump, no frontmatter re-derive). Fixes #948 zero-match phantom write and the #944 phantom last_updated bump. 2. syncStateFrontmatter: extend existing-frontmatter preserve logic — fall back to existingFm['milestone_name'] / existingFm['milestone'] when the derived value is the template placeholder 'milestone' (getMilestoneInfo returns this literal when it cannot match the version in ROADMAP.md); prefer existingFm['stopped_at'] / existingFm['paused_at'] over a body-derived value (the frontmatter value, written by the canonical record-session path, wins over stale historical body lines). Mirrors the fallback already in cmdStateJson. 3. cmdStateRecordSession: when --stopped-at / --resume-file are supplied but body labels are absent, DWIM auto-create a canonical ## Session section (mirroring how add-decision / add-blocker / record-metric auto-create their sections). Never return a silent recorded:false when the caller supplied values. SDK check: no sdk/src/state.ts exists in this repo (the comment in cmdStateSnapshot references a sibling concern in the TypeScript SDK codebase, which is a separate repo not present here). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore: add changeset for PR #952 (fix #948/#944) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#948): correct stopped_at preserve rule; adjust test for sync behaviour The "always prefer frontmatter stopped_at" rule in syncStateFrontmatter was too aggressive — it broke phase.complete which intentionally updates stopped_at in the body and expects syncStateFrontmatter to pick it up. The primary fix (no-op guard in readModifyWriteStateMd) already prevents the stale-body-overwrites-frontmatter scenario from #948: the file is not written when the transform produces no change, so syncStateFrontmatter never runs on a zero-match patch. The body-derived value can only win when an actual write occurs, which means the body was legitimately updated. Reverted to the original #905 rule for stopped_at/paused_at: fall back to existing frontmatter only when the derived value is absent (empty/null). Also adjusted the sync-suite test to assert what state sync actually does (milestone_name preservation) rather than a stopped_at-wins property that state sync does not have by design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#944): update existing session block in place (adversarial review) HIGH finding: the DWIM auto-create in cmdStateRecordSession was appending a second ## Session block unconditionally, even when one already existed with non-canonical content (e.g. a markdown table). Both buildStateFrontmatter and cmdStateSnapshot read only the FIRST ## Session block via regex, so the newly-written Stopped at / Resume file values landed in the second, invisible block — frontmatter stopped_at stayed stale and state-snapshot returned nulls. Fix: check for an existing ## Session heading. When one is present, normalize that section in place by replacing its body with canonical **Last session:** / **Stopped at:** / **Resume file:** bold-label lines. Only append a brand-new section when NO ## Session heading exists. LOW finding: the auto-create scaffold emits **Last session:** but cmdStateSnapshot only matched **Last Date:**, so session.last_date was null after auto-create despite a valid timestamp being written. Fix: extend the lastDateMatch regex in cmdStateSnapshot to also accept **Last session:** / Last session: (the form the scaffold writes). Tests: 3 new tests added to bug-948-state-noop-write-guard.test.cjs that confirmed failure against the previous HEAD and pass after this fix: - exactly one ## Session block after record-session with non-canonical existing block - state-snapshot sees correct stopped_at via first Session block (not a duplicate) - state-snapshot session.last_date is non-null after auto-create on body-less file Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#944): improve in-place section replace to cleanly remove old body content The previous regex `/(^## Session[ \t]*$)([\s\S]*?)(?=\n^## |\n*$)/im` with a lazy match consumed nothing after the heading, so old non-canonical body content (e.g. table rows) remained after the new canonical lines. While functionally correct (parsers found the canonical lines first in the FIRST ## Session block), it left stale content in the section. Replace with a negative-lookahead per-line pattern that consumes all content from the heading up to (but not including) the next ## heading, producing a clean section with only the canonical bold-label lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
d12809985e |
fix(#905): preserve STATE.md frontmatter scalars in syncStateFrontmatter (#907)
syncStateFrontmatter was silently dropping current_phase, current_phase_name, current_plan, and progress when body annotations were absent (e.g. after an agent or tool rewrote the body). These scalars can only be derived from body annotations — when absent, buildStateFrontmatter returns nothing for those keys. Added existingFm fallbacks mirroring the same pattern already applied in cmdStateJson, so every writeStateMd call preserves the existing values instead of stripping them. Also extended cmdStateJson with the same fallbacks for the three non-progress scalars. Adds regression test (7 cases) + lint-test-file-count allowlist entry. Closes #905 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
df04aae5e4 |
enhancement(#537): migrate all hand-written bin/lib/*.cjs to TypeScript source of truth (ADR-457) (#602)
* enhancement(#537): migrate code-review-flags to TS source of truth Collapse the hand-written get-shit-done/bin/lib/code-review-flags.cjs to a TypeScript source of truth (src/code-review-flags.cts), compiled by tsc to a gitignored .cjs build artifact at the same path, per ADR-457 (build-at-publish). Second module after the semver-compare pilot (#541). Behaviour is preserved byte-for-behaviour (characterization test added in tests/code-review-flags.test.cjs locks the parser quirks). Adds compile-time type checking: CodeReviewFlags interface + CodeReviewWorkflow literal union. The require() path is unchanged, so code-review.md and the bug-3727 test keep working. The emitted .cjs is gitignored and eslint-ignored, mirroring the pilot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 9 leaf bin/lib modules to TS source of truth ADR-457 build-at-publish, batch 1 (pure leaf modules, 0 sibling-deps): 001-legacy-orphan-files, context-utilization, redaction, artifacts, command-arg-projection, clock, ui-safety-gate, review-reviewer-selection, clusters. Each moves to src/*.cts (strict TS, typed), compiled by tsc to a gitignored .cjs at the same require() path; behaviour preserved byte-for- behaviour. Adds src/node-globals.d.ts (minimal ambient shim; "types":[]). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#537): add @types/node, drop hand-rolled node-globals shim ADR-457 migration infra: replace the temporary src/node-globals.d.ts ambient shim with @types/node@22 + "types":["node"] in tsconfig.build.json. Unblocks migrating the ~49 remaining bin/lib modules that use node:fs/path/os/ child_process. Build + full suite (3030 pass) + lint all green; no .cts type changes were needed (real Node types matched the shim). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 9 more bin/lib modules to TS (batch 2) ADR-457 build-at-publish. Clean leaves: installer-migration-report, prompt-budget. Type-error-prone leaves (were tsconfig.lint-excluded; now strict-typed and removed from that exclude list): secrets, phase-lifecycle, workstream-name-policy, decisions, validate, schema-detect. Plus runtime-name-policy. Strict type fixes narrow unknown->concrete domain types (no any/ts-ignore); behaviour preserved. Full suite green, lint 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate runtime-slash to TS (cross-import proof) ADR-457. First cross-module TS->TS import: src/runtime-slash.cts imports ./runtime-name-policy.cjs and tsc resolves the sibling .cts types under strict (no declaration files; NodeNext .cjs->.cts mapping), emitting a correct require("./runtime-name-policy.cjs"). Confirms the recipe for coupled modules, which must be migrated in dependency order (leaves-up). Suite green, lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 10 more bin/lib modules to TS (batch 3) ADR-457 build-at-publish, Wave-1 leaves: event, workstream-inventory-builder, plan-scan, fallow-runner, project-root, installer-migration-authoring, update-context, 000-first-time-baseline, runtime-homes, model-catalog. Strict typing fixed real issues (narrowing unknown, qualified fs/path calls, removed unnecessary casts); plan-scan/project-root/workstream-inventory-builder dropped from tsconfig.lint exclude. Behaviour preserved; suite green, lint 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 5 large Wave-1 leaves to TS (batch 4) ADR-457 build-at-publish: configuration, state-document, shell-command- projection (42 dependents), security, command-aliases. shell-command- projection keeps a namespace child_process import for mock-intercept testability. loadConfig/migrateOnDisk emit synchronously (every caller uses them sync; the one awaited migrateOnDisk caller tolerates a non-Promise) — full suite (3030 pass) confirms behaviour preserved. configuration/ state-document/command-aliases dropped from tsconfig.lint exclude. Also fixes the malformed batch-3 changeset frontmatter (type/pr) that failed lint:docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 6 Wave-2 modules to TS (batch 5) ADR-457 build-at-publish: config-schema, model-profiles, 002-codex-legacy-hooks-json, logger, active-workstream-store, adr-parser. First batch importing already-migrated siblings (configuration, model-catalog, shell-command-projection, redaction, security) via ./sibling.cjs specifiers. Strict type narrowing (typeof guards over String(unknown)); behaviour preserved; suite 3030 pass, lint 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 5 large Wave-2 modules to TS (batch 6) ADR-457 build-at-publish: graphify, install-profiles, intel, installer-migrations, worktree-safety. installer-migrations preserves its dynamic require() loader for numbered migration modules (scoped lint suppressions). Strict typing (typeof guards over String(unknown)); behaviour preserved; suite 3030 pass, lint 0 errors. Wave 2 complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate Wave-3 modules to TS (batch 7) ADR-457 build-at-publish: planning-workspace, runtime-artifact-layout, command-routing-hub, drift. Uses `import x = require()` for export= siblings; drift's lazy require of runtime-slash hoisted to a top-level import (verified non-circular). Behaviour preserved; suite 3030 pass, lint 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate small Wave-4 modules to TS (batch 8) ADR-457 build-at-publish: cjs-command-router-adapter, phase-command-router, surface, roadmap-upgrade. Typed the hub router handler results as the HubResult discriminated union; surface drops 4 genuinely-unused imports. Behaviour preserved; suite 3030 pass, lint 0 errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate core hub (2.5k LOC, 68 dependents) to TS (batch 9) ADR-457 build-at-publish: get-shit-done/bin/lib/core.cjs -> src/core.cts, preserving all 63 exports via export=. All sibling deps already migrated (shell-command-projection, model-profiles, model-catalog, worktree-safety, planning-workspace, project-root, configuration, config-schema). Strict types, no any/ts-ignore; config-schema lazy require hoisted (non-circular). Behaviour preserved (independently verified: core's shard 3030 pass / 0 fail). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#537): make ESLint-coverage + test-sprawl checks migration-aware #551 test hardcoded 12 now-migrated modules as "hand-written, must be linted"; that invariant is obsoleted by the ADR-457 migration. Rewrite it to a filesystem-driven invariant that holds at every stage: a bin/lib/*.cjs must be eslint-ignored IFF it has a src/*.cts source (tsc-generated), else linted (covers package-identity, which has no TS source). Also eslint-ignore config-types.cjs (has a src counterpart) and drop the redundant tests/clock.test.cjs (clock already covered by clock-seam + bug-474 tests), which tripped the lint-test-file-count ratchet. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 9 Wave-5 router/inventory modules to TS (batch 10) ADR-457 build-at-publish: phases/verify/init/agent/task/validate/roadmap/state command routers + workstream-inventory. Router handler results typed against core's exported shapes; behaviour preserved (caught+fixed a --verify boolean flag regression mid-migration). Full suite green across all shards (only the 4 local gpg-env changeset-notes failures remain; CI passes them). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 7 Wave-5 modules to TS (batch 11) ADR-457 build-at-publish: gap-checker, docs, check-command-router, frontmatter, learnings, gsd2-import, profile-pipeline. Behaviour preserved; full suite green across all shards (only the 4 local gpg-env failures remain). Also broadens atomic-write-coverage.test.cjs to accept the tsc-compiled namespace-import form while still asserting platformWriteSync is called (safety guard intact). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate config + profile-output to TS (batch 12) ADR-457 build-at-publish: config (729 LOC), profile-output (1142 LOC). All exports preserved; cmdMigrateConfig de-asynced (migrateOnDisk is sync, awaited caller tolerates it). Behaviour preserved; suite green across all shards (only the 4 local gpg-env failures). Wave 5 complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate 5 Wave-6 modules to TS (batch 13) ADR-457 build-at-publish: template, uat, workstream, roadmap, audit. Behaviour preserved (dead toPosixPath import dropped from audit; inline requires hoisted). Suite green across all shards (only the 4 local gpg-env failures). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate commands + state hubs to TS (batch 14) ADR-457 build-at-publish: commands (1305 LOC), state (2074 LOC, 17 dependents). All exports preserved; inner requires kept non-hoisted where load-order matters (install.js, per-call security); acquireStateLock cast inlined to preserve the err.code source token a structural test inspects. Behaviour preserved; suite green across all shards (only the 4 local gpg-env failures). Wave 6 complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate milestone to TS (batch 15a, hand-authored) ADR-457 build-at-publish: milestone -> src/milestone.cts. Authored directly (subagent capacity was unavailable). Also relaxes core.output()'s 3rd param to optional, matching its real always-optional call contract (unblocks remaining 2-arg output callers). Behaviour preserved; suite green across all shards (only the 4 local gpg-env failures). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537): migrate phase, verify, init to TS (batch 15, final modules) ADR-457 build-at-publish, Wave 7 (the last hubs): phase (1608 LOC), verify (1615), init (2113). Adds src/package-identity.d.cts so verify can import the permanently value-baked package-identity.cjs under strict TS. Fixes two regressions the migration introduced in verify: restore cmdValidateHealth's `return result` (callers/tests read result.warnings — it is NOT side-effect-only), and make the bug-3384 source-pattern test tolerant of the tsc-compiled bracket-notation form of the git_list_failed->W020 branch (behaviour intact). Full suite green across all shards (only the 4 local gpg-env failures); lint 0 errors. All 86 migratable bin/lib modules are now TypeScript sources. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#537): finalize ADR-457 migration — retire tsconfig.lint.json All hand-written bin/lib/*.cjs are now src/*.cts sources, so the checkJs stopgap tsconfig.lint.json (unused; not wired into eslint, scripts, or CI) is deleted per ADR-457's final step. Also gitignore the tsc-generated config-types.cjs (was still committed) for consistency with every other emitted artifact. package-identity.cjs stays value-baked (declared via src/package-identity.d.cts). Suite green; #551 ESLint-coverage test green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): add prepare script so unpacked/git installs build bin/lib artifacts ADR-457 build-at-publish: bin/lib/*.cjs are now gitignored, built by tsc. The prepack/prepublishOnly hooks cover `npm pack`/publish, but `npm install -g <dir>` and git installs run the `prepare` lifecycle — which was missing — so the unpacked install shipped without the compiled .cjs and failed at startup with "Cannot find module './lib/core.cjs'" (caught by the smoke-unpacked CI job). Add `prepare` mirroring prepublishOnly (build:lib + build:hooks). prepare does NOT run for registry consumers (they get the pre-built tarball), only for source/local/pack installs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): make CI build/lockfile checks work with gitignored bin/lib artifacts ADR-457 build-at-publish exposed two CI assumptions that bin/lib/*.cjs are always present on disk: - check:env's lockfile-sync ran `npm ci --dry-run`, which now triggers the `prepare` build (tsc) — but it runs before deps are installed, so tsc is absent and it misreported the lockfile as out of sync. Add --ignore-scripts (a lockfile check must not build). - the lint-tests job installs with --ignore-scripts (no prepare build), but lint:skill-deps require()s the built install-profiles.cjs. Add an explicit `npm run build:lib` step after install. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): narrow prepare to build:lib only (unbreak packed-smoke pack step) prepare running build:hooks emitted "✓ Copying ..." stdout during `npm pack`, which the install-smoke "Pack root tarball" step captures into $GITHUB_OUTPUT — breaking it with "Invalid format". build:lib (tsc) is silent on success and is all the unpacked/source install needs (the smoke-unpacked assertions exercise gsd-tools, i.e. bin/lib, and tolerate hook setup with `|| true`). Matches prepack. build:hooks still runs on prepublishOnly for real publishes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#537): wire Stryker mutation gate to build-at-publish layout The gate scored 0.00 because it mutated changed bin/lib/*.cjs that (a) were generated artifacts and (b) included modules with no coverage in the command's test set. Rework: mutation.yml now derives changed COVERED modules from src/*.cts and maps them to their built bin/lib/*.cjs; Stryker mutates those built artifacts with a no-rebuild command (mutating src/*.cts + per-mutant tsc was ~3x over the 30-min CI budget). NOTE: with the gate now correctly measuring the covered modules, their actual mutation score is 42.94% (< break 50) — a pre-existing test-coverage gap (adr-parser/prompt-budget/etc.), not introduced by this behaviour-preserving migration. Reaching 50 needs more tests, a threshold/scope change, or a waiver — a maintainer decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#537): raise mutation coverage of covered modules above the 50 gate Adds focused example-based unit tests that kill surviving mutants in the two lowest-scoring covered modules: - tests/prompt-budget.unit.test.cjs (112 tests): 17.9% -> 97.9% - tests/adr-parser.unit.test.cjs (205 tests): 44.7% -> 89.4% Both wired into stryker.config.mjs's command. Fresh full run over the 6 covered modules now scores 82.25% (>= break 50); every covered module is >= 68%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * enhancement(#537,#609): parallelize mutation gate via dynamic per-module matrix The serial Stryker run timed out at 30 min once the migration's added tests made every mutant re-run ~300 tests. Replace it with a dynamic matrix so the gate completes well under budget — folded into this PR (was tracked as #609) because it's a prerequisite for this PR's mutation gate to pass. - scripts/mutation-matrix.cjs: single source of truth (covered-module -> test files) computing changed covered modules from git diff -> {has_work, matrix}. - mutation.yml: detect -> dynamic `matrix: fromJSON(...)` mutate job (one parallel shard per changed module, scoped via MUTATION_TEST_CMD to only that module's tests, 15-min/shard) -> summary job that KEEPS the legacy check name "Stryker mutation score (changed files only)" so branch protection is unchanged. Per-shard jobs report as "Stryker (<module>)". - stryker.config.mjs: commandRunner.command reads MUTATION_TEST_CMD (falls back to the full command locally). Closes #609. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#537,#609): give each mutation shard ≥50% on its own tests; drop blacksmith note Per-module sharding revealed that active-workstream-store (46.5%) and frontmatter (7.4%) only cleared 50% in the old serial run via timeout-noise from the bloated 300-test command; on their own tests they were below the gate. Add focused unit tests: - tests/active-workstream-store.unit.test.cjs (115 tests): 46.5% -> 81.9% - tests/frontmatter.unit.test.cjs (165 tests): 7.4% -> 63.4% Both wired into scripts/mutation-matrix.cjs (per-module test map) and stryker.config.mjs DEFAULT_TEST_CMD. All 6 covered modules now clear break:50 with only their own tests (config-schema/context-utilization/prompt-budget/ adr-parser already did). Also removes the leftover blacksmith TODO comment — GitHub-hosted runners only; speed comes from parallel per-module shards. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#537,#609): strengthen prompt-budget tests to clear the gate on its own tests prompt-budget scored 39.58% when mutation-tested with ONLY its own tests (the way the per-module CI shard runs it) — an earlier ~98% reading was inflated by accidentally running the full multi-module command. Add 96 targeted tests to tests/prompt-budget.unit.test.cjs (exact note-template text, plan-truncation arithmetic/percentages, drop-block strings, noteInjected/hardFailed booleans): scoped score 39.58% -> 68.75% (>= break 50). All 6 covered modules now clear the gate on their own tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |