* test(#2754): parseStateMd must parse CRLF STATE.md identically to LF
The frontmatter fence regex and downstream splits used literal \n, so a CRLF
STATE.md dropped the ENTIRE frontmatter block. Assert CRLF/LF parity (the
production contract) across full frontmatter, next_phases flow + block forms,
the progress nested block, and null handling.
* fix(#2754): make parseStateMd CRLF-safe — frontmatter fence + splits use \r?\n
The fence regex, scalar-line split, next_phases block-list regex, and progress
block regex all used literal \n, so a CRLF (Windows-authored) STATE.md dropped
the ENTIRE frontmatter block — every statusline field was silently absent. Use
\r?\n throughout, mirroring the CRLF-safe extractFrontmatter in src/frontmatter.cts.
LF behavior unchanged.
* chore(#2754): changeset fragment
* test(#2754): pin parseStateMd↔extractFrontmatter parity (Generative-Fix Divergence guard)
The statusline parseStateMd and the canonical extractFrontmatter both derive GSD
state from STATE.md frontmatter and diverged once already (the CRLF bug this PR
fixes). Add a cross-parser parity assertion (CLAUDE.md parallel-surfaces rule)
over the overlapping fields, under both LF and CRLF, so a future divergence on a
scalar shape or line ending is caught here. (isolated-review minor finding)
* chore(#2754): backfill changeset PR number (2865)
---------
Co-authored-by: Test <test@example.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>
- 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
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>
Fold 27 issue-named installer/hooks/statusline/migration/reapply regression files
into their canonical module suites (installer-migrations, installer-migration-report,
gsd-statusline, reapply-verify-hunks, install-*, gsd-check-update-worker-platform-gate,
etc.). Verbatim block-scoped describe wrappers; 276 subtests conserved 1:1. No new files.
The one subdir origin (tests/installer-migrations/001-legacy-orphan-files) moved up one
level into installer-migrations.test.cjs; its single ../../ module require corrected to
../ so it resolves from tests/ root (verified). Host-env pre-check: no CLI-receiving host
sets a redirecting GSD_WORKSTREAM/GSD_PROJECT value.
Regenerates regression-name allowlist (222->205), ratchets file-count allowlist (verify
11->8, validate entry removed), makes 16 relocated allow-test-rule exemptions issue-ref-
compliant (ADR-456; prunes stale ids). Repoints 15 tests/ references across state-md.md
(EN + ja/ko/pt/zh). lint:ci green.
Part of epic #1969. Closes#1974.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#1194): correct inverted statusline auto-compact buffer math
The reserved-buffer percentage was computed as (acw/totalCtx)*100 — the
usable fraction — instead of (1 - acw/totalCtx)*100 — the reserved fraction.
When acw == totalCtx this produced buffer=100%, making the usable-range
denominator zero and pinning `used` at a constant 100% regardless of real
remaining context.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: backfill changeset PR number (#1211)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 5 no-control-regex warnings are all the same intentional ANSI-color-strip
pattern /\x1b\[[0-9;]*m/g across 5 test files. The \x1b (ESC) control char is
the required leading byte of an ANSI SGR sequence, so matching it is the whole
point of stripping color codes from captured CLI/console output. Add an inline
eslint-disable-next-line with justification at each site (not a refactor — the
control char is essential, not accidental), then flip no-control-regex from
warn to error so the debt can't regrow.
Refs #736
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The windows-test-parity ratchet greps test source for fs.rmSync-without-
maxRetries (and six other Windows-portability anti-patterns), failing when an
integer offender COUNT exceeds a frozen baseline (rmSync: 95). A count ratchet
is a Goodhart metric: fixing one offender and adding another keeps the count
constant, so a new defect slips through green. Replace it — and every other
count ratchet in the repo — with a layered, masking-proof design.
Behavioral seam test
- tests/helpers-cleanup.test.cjs proves helpers.cleanup() carries the Windows
EBUSY retry budget. cleanup() delegates retries to Node's fs.rmSync via
maxRetries (it owns no loop), so the test asserts the option contract
(recursive/force/maxRetries>0/retryDelay>0) + real-FS removal + the cwd-guard,
rather than a loop that does not exist. The EBUSY risk is now tested ONCE at
the helper, not approximated textually at every call site.
Write-time ESLint rule (AST-accurate, replaces the grep)
- eslint-rules/no-raw-rmsync-in-tests.cjs (error in tests/**/*.test.cjs) bans
raw fs.rmSync, steering to cleanup(). Catches member, computed (fs['rmSync']),
destructured and aliased forms; escape hatch is inline
`// eslint-disable-next-line local/no-raw-rmsync-in-tests -- <reason>` only.
- Migrated 336 raw fs.rmSync teardown calls across ~116 test files to cleanup().
~18 genuinely load-bearing sites (mid-test SUT/fault-injection removals,
error-swallowing or name-colliding local teardown helpers) keep the raw call
with an inline eslint-disable + reason.
Shared anti-ratchet primitive
- scripts/lib/allowlist-ratchet.cjs:
- assertWithinAllowlist: fails on NOVEL ids (new offender introduced) AND on
STALE ids (a known offender was fixed but not pruned) — identity, not count,
and a ratchet DOWN toward zero.
- assertTightCeiling: a size/length budget whose ceiling must stay within a
grace band of the high-water mark, so budgets may only tighten, never creep.
Ratchets converted onto the primitive
- windows-test-parity-guard.test.cjs: rmSync rule deleted (now ESLint-enforced);
the remaining six patterns moved from integer baselines to named-set
allowlists with ratchet-down.
- scripts/lint-test-file-count.{cjs,allowlist.json}: per-module integer counts →
named filename sets (closes the swap-a-file-keep-the-count blind spot); a
module dropping under cap now FAILS to force pruning its allowlist entry.
- enh-2790 skill-count `<= 63` → named skill allowlist (ratchets toward ~58).
Size budgets hardened (tighten-only)
- agent-size / workflow-size / feat-3039 help-tiered: ceilings lowered to the
current high-water mark and an assertTightCeiling anti-creep check added per
tier. Fixed external-contract limits (description ≤100 chars, agent ≤100 KB)
are intentionally left as-is — they are not grandfathered creeping budgets.
No user-facing behavior change (tests + tooling only); no USER_FACING_PREFIXES
touched, so no changeset fragment is required.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Replace the per-render readdirSync().filter().map(statSync).sort() chain
with a single-pass max-by-mtime loop. Drops the O(n log n) sort and the
throwaway intermediate array; I/O and resolved-file behavior are identical.
Adds the first behavior-lock test for the todo-resolution path.
The larger disk-backed cache win from the issue is deferred: statusline is
a fresh child process per render, so any cache must be disk-backed with
invalidation/atomic-write design that needs maintainer input.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug #2453: After tsc builds sdk/dist/cli.js, npm install -g from a local
directory does not chmod the bin-script target (unlike tarball extraction).
The file lands at mode 644, the gsd-sdk symlink points at a non-executable
file, and command -v gsd-sdk fails on every first install. Fix: explicitly
chmodSync(cliPath, 0o755) immediately after npm install -g completes,
mirroring the pattern used for hook files throughout the installer.
Bug #2451: gsd-context-monitor warning messages over-reported usage by ~13
percentage points vs CC native /context. Root cause: gsd-statusline.js
wrote a buffer-normalized used_pct (accounting for the 16.5% autocompact
reserve) to the bridge file, inflating values. The bridge used_pct is now
raw (Math.round(100 - remaining_percentage)), consistent with what CC's
native /context command reports. The statusline progress bar continues to
display the normalized value; only the bridge value changes. Updated the
existing #2219 tests to check the normalized display via hook stdout rather
than bridge.used_pct, and added a new assertion that bridge.used_pct is raw.
Closes#2453Closes#2451
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The autocompact buffer percentage was hardcoded to 16.5%. Users who set
CLAUDE_CODE_AUTO_COMPACT_WINDOW to a custom token count (e.g. 400000 on
a 1M-context model) saw a miscalibrated context meter and incorrect
warning thresholds in the context-monitor hook (which reads used_pct from
the bridge file the statusline writes).
Now reads CLAUDE_CODE_AUTO_COMPACT_WINDOW from the hook env and computes:
buffer_pct = acw_tokens / total_tokens * 100
Defaults to 16.5% when the var is absent or zero, preserving existing
behavior.
Also applies the renameDecimalPhases zero-padding fix for clean CI.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When no in_progress todo is active, fill the middle slot of
gsd-statusline.js with GSD state read from .planning/STATE.md.
Format: <milestone> · <status> · <phase name> (N/total)
- Add readGsdState() — walks up from workspace dir looking for
.planning/STATE.md (bounded at 10 levels / home dir)
- Add parseStateMd() — reads YAML frontmatter (status, milestone,
milestone_name) and Phase line from body; falls back to body Status:
parsing for older STATE.md files without frontmatter
- Add formatGsdState() — joins available parts with ' · ', degrades
gracefully when fields are missing
- Wrap stdin handler in runStatusline() and export helpers so unit
tests can require the file without triggering the script behavior
Strictly additive: active todo wins the slot (unchanged); missing
STATE.md leaves the slot empty (unchanged). Only the "no active todo
AND STATE.md present" path is new.
Uses the YAML frontmatter added for #628, completing the statusline
display that issue originally proposed.
Closes#1989