Commit Graph

4542 Commits

Author SHA1 Message Date
Tom Boucher
4ad7977093 docs(#2194): add changeset fragment 2026-07-12 18:53:49 -04:00
Tom Boucher
24c324cbfa fix(#2194): add Bash timeout guidance for prompt-fed reviewers in review.md
The Gemini, Claude, and Codex reviewer blocks invoked the CLIs with no explicit
timeout, so each inherited the host default (~2 min on Claude Code). A source-
grounded review of a large plan set takes ~570s (Codex xhigh) / ~525s (headless
Claude) — both exceed that window, so the lane is killed mid-review, its output
is empty, and the cross-AI review silently proceeds with fewer lanes. CodeRabbit
and OpenCode already documented a timeout; the four main lanes did not.

Add a shared timeout-guidance note directing a high Bash timeout (>= 900000;
1200000 for Codex xhigh / headless Claude), referencing BASH_MAX_TIMEOUT_MS for
the Claude Code host cap, and framing a slow-lane empty output as a timeout kill
(not the 0xc0000142 crash it gets misdiagnosed as) so operators re-run with more
time instead of diagnosing a CLI failure.

Closes #2194

Recaptures the 18 golden-install-parity fixtures + workflow-size baseline (only
the review.md entry changed in each; review is LARGE-tier, 47168 < 61440).
2026-07-12 18:53:49 -04:00
Tom Boucher
26b921720c test(#2194): guard prompt-fed reviewer timeout guidance in review.md
Source-text contract guard: review.md IS the product the runtime loads. Assert
its reviewer-invocation section carries Bash timeout guidance (>= 900000ms) for
the Gemini/Claude/Codex lanes and frames a slow-lane empty output as a timeout
rather than a crash.
2026-07-12 18:53:49 -04:00
Tom Boucher
605984cd79 test(#2177): use lint-clean .includes() for frontmatter-survival assertions
CI lint (local/no-crlf-fragile-split + no-regex-spaces) flagged the regex
assertions: a bare \n on readFileSync content is CRLF-fragile on Windows
git-autocrlf, and literal double-spaces are hard to count. Switch to .includes()
strings — same validation intent, lint-clean on all platforms.
2026-07-12 17:17:24 -04:00
Tom Boucher
41d093bcb8 docs(#2177): backfill PR number in changeset fragment 2026-07-12 17:17:24 -04:00
Tom Boucher
ccd7fe1af9 docs(#2177): add changeset fragment 2026-07-12 17:17:24 -04:00
Tom Boucher
e8533b875d fix(#2177): match the body Progress: line, preserve the descriptive suffix
cmdStateUpdateProgress ran its Progress: patterns against the raw STATE.md
content (frontmatter included). With /i+/m the first match was the YAML
frontmatter `progress:` key, not the body line — so the frontmatter block was
mangled (\s* crossed the newline) while the body line stayed stale, and the
next STATE.md write re-derived percent from that stale line, silently reverting
the update. \2: the .* also discarded any agent-authored descriptive suffix.

- Match against the frontmatter-stripped body only (reusing stripFrontmatter);
  reconstruct content as fmPrefix + modified body so the frontmatter is untouched.
- Swap only the machine segment ([bar] NN% or bare NN%), preserving any suffix.
- updated stays false when the body has no Progress: line (no false success from
  a frontmatter progress: key).

Closes #2177
2026-07-12 17:17:24 -04:00
Tom Boucher
dbec81d5ad test(#2177): cover frontmatter progress: shadow + suffix preservation
Adds the frontmatter-bearing STATE.md fixture the suite lacked. Asserts the
body Progress: line (not the YAML progress: key) is the update target, the
descriptive suffix after [bar] NN% is preserved, the frontmatter block is not
mangled, and a body with no Progress: line reports updated:false even when the
frontmatter has a progress: key (no false success).
2026-07-12 17:17:24 -04:00
Tom Boucher
4682418ae4 docs(#2185): backfill PR number in changeset fragment 2026-07-12 16:46:37 -04:00
Tom Boucher
631a9d4cef docs(#2185): add changeset fragment 2026-07-12 16:46:37 -04:00
Tom Boucher
7baabad146 fix(#2185): normalize Linuxbrew + any Homebrew Cellar path to the stable symlink
normalizeNodePath had hardcoded branches for macOS Intel (/usr/local/Cellar) and
Apple Silicon (/opt/homebrew/Cellar) but none for Linuxbrew
(/home/linuxbrew/.linuxbrew/Cellar). After `brew upgrade node` on Linux, the
version-pinned Cellar path baked into managed hook commands 404'd, and re-running
the installer did not repair it — normalizeNodePath returned the pinned path
unchanged, so the 'already normalized' skip-rewrite check no-op'd.

Generalize to one branch: match <prefix>/Cellar/node(<@ver>)?/<ver>/bin/node and
rewrite to <prefix>/bin/node, deriving <prefix> from the path itself. This covers
macOS Intel, Apple Silicon, Linuxbrew, and any custom HOMEBREW_PREFIX — the same
failure class as #977 (fnm) and #1619 (mise), now closed for Homebrew on every
platform.

Closes #2185
2026-07-12 16:46:37 -04:00
Tom Boucher
dae8e70a5a test(#2185): cover Linuxbrew + custom-prefix Cellar paths in normalizeNodePath
Adds the Linuxbrew Cellar layout (/home/linuxbrew/.linuxbrew/Cellar/...), a
post-version-bump path, a node@version formula, and a custom HOMEBREW_PREFIX —
all mapping to the stable <prefix>/bin/node symlink. Mirrored in both
consolidated describe blocks.
2026-07-12 16:46:37 -04:00
Tom Boucher
6a25d2f437 docs(#2152): backfill PR number in changeset fragment 2026-07-12 16:36:36 -04:00
Tom Boucher
5da620fe34 docs(#2152): add changeset fragment 2026-07-12 16:36:36 -04:00
Tom Boucher
c4f17a8d6e fix(#2152): accept a homedir option in readGsd(Effective|Global)ModelOverrides
readGsdEffectiveModelOverrides resolved ~/.gsd/defaults.json via os.homedir()
with no seam, so a test asserting project-only overrides could not isolate the
global file. Add an optional { homedir } option (defaults to os.homedir()) to
readGsdGlobalModelOverrides and readGsdEffectiveModelOverrides — the same
dependency-injection shape the sibling warnIfStaleBake already uses. Backward-
compatible: existing callers pass no option and behave identically.

Closes #2152
2026-07-12 16:36:36 -04:00
Tom Boucher
5bce5c46a6 test(#2152): sandbox HOME in readGsdEffectiveModelOverrides subtest
The subtest asserted project-only model_overrides but called
readGsdEffectiveModelOverrides without isolating HOME, so the real
~/.gsd/defaults.json global overrides bled into the deepEqual on any dev box or
non-hermetic runner that has one. Pass a sandboxed homedir (mirroring the sibling
warnIfStaleBake subtests that already inject homedir).
2026-07-12 16:36:36 -04:00
Tom Boucher
e3231717d0 docs(#2150): backfill PR number in changeset fragment 2026-07-12 16:22:47 -04:00
Tom Boucher
e6f4bf3e77 fix(#2150): line-anchor + word-boundary the UI hint regex (review L1/L2)
Review found the hint value (yes|no) matched prefixes ('nope'/'not' read as
'no') and the hint regex was unanchored, so a mid-line prose mention like 'see
**UI hint**: no above' was treated as the authoritative metadata line. Line-
anchor (^ + m flag) so only a real hint line counts; word-boundary on the value
so nope/not do not mean no. Adds coverage for hint:yes over a pure-backend body
and the nope fall-through.
2026-07-12 16:22:47 -04:00
Tom Boucher
81ffd12967 docs(#2150): add changeset fragment for UI hint authority fix 2026-07-12 16:22:47 -04:00
Tom Boucher
0e59e9f05b fix(#2150): treat the UI hint yes/no line as authoritative in checkUiPresence
checkUiPresence ran UI_TOKENS (which includes the bare token 'UI') over the raw
phase-section text, so GSD's own '**UI hint**: no' metadata line matched the 'UI'
token and reported hasUI=true — blocking non-frontend phases that explicitly
declare themselves non-frontend via the documented convention (the plan-phase
UI-SPEC gate fired under the default workflow.ui_safety_gate=true).

- An explicit '**UI hint**: yes|no' line is now authoritative (mirrors how
  progress.md / new-project.md already parse it via 'UI hint.*yes'). hint:no ->
  hasUI=false; hint:yes -> hasUI=true.
- Any '**UI hint**:' line is stripped before token-sniffing, so a hint without a
  recognised yes/no cannot false-positive on the bare 'UI' token.
- With no hint line, behaviour is unchanged (token-sniffing on the rest).

Closes #2150
2026-07-12 16:22:47 -04:00
Tom Boucher
578a7fbc17 test(#2150): cover UI hint yes/no authority in checkUiPresence
Adds the divergent input the green suite lacked: the documented `**UI hint**:
no` metadata line. Asserts hint:no is authoritative non-frontend (no false
positive), hint:yes is authoritative frontend, a malformed hint is stripped so
its bare UI token does not fire, and hint:no overrides genuine UI language.
2026-07-12 16:22:47 -04:00
Tom Boucher
4c9fde8aa5 docs(#2140): backfill PR number in changeset fragment 2026-07-12 16:06:29 -04:00
Tom Boucher
b94f8754fa fix(#2140): scope hasRow to traceability-row shape (review L2)
hasRow keyed on a bare '| ID |' which could match the ID as the first cell of a
non-traceability table elsewhere in REQUIREMENTS.md, suppressing a real
table_unmatched signal. Require a second cell ('| ID | <phase> |') so only a
traceability-row shape counts as a row.
2026-07-12 16:06:29 -04:00
Tom Boucher
32b5262f7b docs(#2140): add changeset fragment for requirements mark-complete fix 2026-07-12 16:06:29 -04:00
Tom Boucher
87ab4a4896 fix(#2140): distinguish checkbox-only reconcile from full in requirements mark-complete
cmdRequirementsMarkComplete OR-ed its two write surfaces (the - [ ] checkbox and
the | ID | ... | Pending | traceability row) into one 'found' flag. When the
checkbox matched and no table row did, it reported the same 'updated: true,
marked_complete: [ID]' payload as a full reconcile — while the traceability row
stayed Pending. The already_complete branch used OR (checkbox done OR row done),
so re-running on the half-written file classified the drift as already_complete
and moved on. audit-milestone (which reads the table) still saw Pending.

- Track the two surfaces separately (checkboxHit / tableHit).
- Add a 'table_unmatched' bucket: an ID whose checkbox reconciled (this run or
  before) but whose traceability table has no row for it — only when a table
  actually exists (a table-less REQUIREMENTS.md is legitimate).
- Fix the already_complete predicate: fully reconciled requires the row Complete,
  OR the checkbox done with NO table — a [x] checkbox with an absent/Pending row
  is partial, not done.
- A table-less REQUIREMENTS.md stays a clean success (the OR's legitimate use).

The invariant: a not-fully-applied requirement no longer returns a payload
indistinguishable from a fully-applied one.

Closes #2140
2026-07-12 16:06:29 -04:00
Tom Boucher
cee8d7d723 test(#2140): cover checkbox/table divergence in requirements mark-complete
Adds the fixture the green suite lacked: an ID with a checkbox and a traceability
table that does NOT mention it. Asserts (1) a checkbox-only reconcile surfaces
table_unmatched instead of a silent full-success payload, (2) re-run on the
half-written file does NOT mask the drift as already_complete, and (3) a
REQUIREMENTS.md with no traceability table stays a clean success.
2026-07-12 16:06:29 -04:00
Tom Boucher
9208c40127 docs(#2138): backfill PR number in changeset fragment 2026-07-12 15:55:39 -04:00
Tom Boucher
924ff6822e fix(#2138): surface track_shipping push failures (review)
Review (MEDIUM): `git push ... 2>&1` did not check exit code, so a silent push
failure (auth-token expiry, network blip, non-fast-forward) would proceed to the
report step and declare success — silently reproducing the exact #2138 defect.
Add a fallback warning naming the rerun command so a failed push is visible
(best-effort: the PR already exists, so we still report it rather than abort).
Recaptures goldens + size baseline.
2026-07-12 15:55:39 -04:00
Tom Boucher
7837d67362 docs(#2138): add changeset fragment for ship-note push fix 2026-07-12 15:55:39 -04:00
Tom Boucher
aaf74878f7 fix(#2138): push the track_shipping ship-note onto the PR branch [ci skip]
track_shipping committed the STATE ship-note ('Phase N shipped — PR #N') AFTER
create_pr and never pushed it, so the commit stayed local-only. When the GitHub
PR merged (especially fast/auto-merge) the ship-note was not in the source branch
and never reached the default branch — STATE's ship-status was silently lost,
recoverable only by STATE self-heal on the next /gsd-start.

Push the ship-note commit onto the PR branch with a [ci skip] trailer. GitHub
honors [ci skip]/[skip ci], so this lands the note on merge without triggering a
redundant pipeline, and preserves the PR number in STATE.

Recaptures the 18 golden-install-parity fixtures + the workflow-size baseline
(only the ship.md entry changed in each).

Closes #2138
2026-07-12 15:55:39 -04:00
Tom Boucher
69ac4d0aa7 test(#2138): guard track_shipping pushes the ship-note (source-text contract)
ship.md IS the product the runtime loads. Assert its track_shipping step pushes
the committed ship-note onto the PR branch and carries a [ci skip] trailer, so a
regression to the local-only commit (the #2138 bug) is caught.
2026-07-12 15:55:39 -04:00
Tom Boucher
b145ff6177 docs(#2136): backfill PR number in changeset fragment 2026-07-12 15:34:59 -04:00
Tom Boucher
58b20c31f8 fix(#2136): migrate ALL operator-facing date sites to localToday (anti-pattern elimination)
The UTC-slice anti-pattern (deriving a calendar day a human reads by slicing a
UTC instant) remained in several operator-facing sites beyond the original
seamed last_activity set. Eliminate it everywhere a human reads the value as a
calendar day — do not leave known bad code in place:

- commands.cts cmdTodoComplete + cmdScaffold: completion/scaffold dates.
- gsd2-import.cts: migrated STATE.md 'Last activity' / 'Last session'.
- template.cts: plan frontmatter 'completed:' date.
- verify.cts: health --repair session-log date + '(Backfilled: <date>)' header.
- workstream.cts: workstream-create 'Last Activity' / 'created' + archive dirname.
- init.cts: JSON-bundle 'date' (-> localToday) + 'timestamp' (-> nowIso) at all
  three sites; drop the now-dead 'const now = new Date()' in cmdInitTodos /
  cmdInitMapCodebase (cmdInitQuick keeps it for the local branch-id derivation).
- state.cts: prune-archive '## Pruned <date>' header.
- state-transition.cts: the 7 seamed last_activity writes (prior commit).

No realClock.today() / .clock.today() / raw new Date().toISOString().split('T')
operator-facing sites remain in src/. Rebuilds the tracked
bin/lib/state-transition.cjs artifact to match.
2026-07-12 15:34:59 -04:00
Tom Boucher
9ab328917b docs(#2136): add changeset fragment for local calendar day fix 2026-07-12 15:34:59 -04:00
Tom Boucher
71edb27fe1 fix(#2136): route operator-facing date fields through local clock day
clock.today() derived the calendar day by slicing a UTC ISO instant
(nowIso().split('T')[0]), so in any negative-UTC-offset zone during UTC's early
hours last_activity named a day the operator had not reached yet — ahead of
last_updated's local date, written by the same call.

- Add Clock.localToday(): host-local YYYY-MM-DD via getMonth/getDate/
  getFullYear, honoring the same GSD_NOW_MS pin as today()/nowIso().
- Route operator-facing date-only fields through localToday(): last_activity
  (state-transition ×7, state.cts), roadmap 'completed <date>' (phase.cts,
  roadmap.cts), milestone completion date (milestone.cts), todo/scaffold
  completion (commands.cts — now threaded through the realClock seam).
- Leave today() (UTC) as the source for internal/cosmetic stamps.

Closes #2136
2026-07-12 15:34:59 -04:00
Tom Boucher
51b6e3c35c test(#2136): add localToday regression + mirror fake/inline clock stubs
Adds a dedicated regression (tests/fix-2136-clock-local-today.test.cjs):
- realClock.localToday() returns the LOCAL calendar day under a pinned instant
  + TZ (America/Chicago → 2020-06-14, the issue's repro instant).
- realClock.today() is unchanged (UTC) — internal/cosmetic stamps stay UTC.
- localToday === today on a UTC host (no spurious divergence).
- makeFakeClock mirrors localToday (drop-in Clock substitute).
- field-level: a 'sync' transition with a split clock (today≠localToday)
  writes the LOCAL day into Last Activity, proving the wiring uses localToday.

Mirrors localToday() in the fake clock helper and the inline fixedClock stubs
in state-transition.test.cjs / state-rebuild.test.cjs (the Clock interface now
requires it).
2026-07-12 15:34:59 -04:00
Tom Boucher
09a7cc9fea Merge pull request #2215 from open-gsd/fix/2135-milestone-name-clobber
fix(#2135): anchor milestone heading regex + strip delimiter, widen preserve guard
2026-07-12 14:21:50 -04:00
Tom Boucher
58b2aa1985 Merge branch 'next' into fix/2135-milestone-name-clobber 2026-07-12 12:59:02 -04:00
Tom Boucher
a02fe1213c Merge pull request #2214 from open-gsd/fix/2133-fast-md-log-to-state-schema-gate
fix(#2133): correct fast.md log_to_state column-count gate (NF-2)
2026-07-12 12:58:46 -04:00
Tom Boucher
a5110c4b8c Merge branch 'next' into fix/2133-fast-md-log-to-state-schema-gate 2026-07-12 12:42:15 -04:00
Tom Boucher
d474b5010d docs(#2135): backfill PR number in changeset fragment 2026-07-12 11:59:51 -04:00
Tom Boucher
af8d650da9 docs(#2135): add changeset fragment for milestone_name clobber fix 2026-07-12 11:41:14 -04:00
Tom Boucher
3bc53c8116 fix(#2135): anchor milestone heading regex + strip delimiter, widen preserve guard
getMilestoneInfo's `##` heading regex was unanchored (no `^`/`m`), so it
matched a `##` quoted mid-line inside a Milestones bullet and captured a
delimiter-led fragment into milestone_name — clobbering the curated name on
every phase transition.

roadmap-parser.cts (load-bearing):
- Consult the 🚧 name-bearing marker FIRST (reorder; it already existed but was
  shadowed by a spuriously-successful heading match).
- Anchor the `##` regex to line start (`^` + `m` flag) so a heading quoted
  in backticks/prose can no longer match.
- stripLeadingDelimiter removes a leading em/en-dash/colon/hyphen run that
  .trim() cannot (the `## vX.Y — Name` convention).

state.cts (defense in depth):
- Widen the #948 preserve guard from 'derived equals placeholder' to
  'derived does not look like a name' (non-empty, not placeholder, not
  punctuation-led), so a future bad derive preserves the curated name instead
  of silently overwriting it.

Closes #2135
2026-07-12 11:39:59 -04:00
Tom Boucher
5cbe250f36 test(#2135): add 5-case regression for getMilestoneInfo name derive
Adds the issue's verification matrix (cases A-E): a 🚧 bullet quoting a
nameless ## heading in backticks (the corruption), a nameless heading plus a
🚧 sub-heading carrying the name, canonical colon/em-dash shapes (no
regression), a 🚧 bullet only, and an anchored-regex guard proving a ##
heading quoted mid-line in backticks never matches.
2026-07-12 11:39:59 -04:00
Tom Boucher
f5370ef9c0 docs(#2133): backfill PR number in changeset fragment 2026-07-12 11:26:24 -04:00
Tom Boucher
cb48b2b48f test(#2133): drop no-op try/finally in runAgainst helper (review) 2026-07-12 11:13:48 -04:00
Tom Boucher
30469ba751 test(#2133): drop unused os import (lint clean) 2026-07-12 11:08:20 -04:00
Tom Boucher
c6b4304bab test(#2133): regenerate golden + workflow-size baselines for fast.md
The fast.md log_to_state fix changes an installed workflow file, so the per-
runtime golden-install-parity fixtures (18 runtimes) and the workflow-size
baseline are recaptured. Diff is exactly one entry per fixture (the fast.md
hash) and one baseline byte count — no spurious drift.
2026-07-12 10:48:43 -04:00
Tom Boucher
dca8bd9daa test(#2133): scope NF-2 assertion to the executable awk formula
The first draft banned the literal /NF-1/ anywhere in the block, but the
explanatory comment legitimately references 'NF-1' to document the off-by-one
root cause. Match the COL_COUNT awk assignment specifically so the structural
guard targets the executable formula, not the prose.
2026-07-12 10:48:43 -04:00
Tom Boucher
6504afa843 Merge pull request #2213 from open-gsd/fix/2116-surface-bare-require
fix(#2116): use resolvable paths in surface.md require + correct package name
2026-07-12 10:41:41 -04:00