* test(#2734): failing-first suite for the statusline STATE.md freshness marker
Binds the contract before any hook change exists: a `state ~N commits back`
segment gated on the state_head stamp landed by #2622, firing at the same
advisory threshold /gsd-health's W024 uses rather than at > 0.
Covers all five acceptance criteria — threshold parity (19/20/21 boundaries),
both renderers including formatGsdStateCompact, an exact spawn-count assertion,
repo-pinning and sub_repos degradation, and behavioral parity against
readStateHeadFreshness rather than a source-grep of the two fence copies.
52 example-based tests plus 5 seeded fast-check properties. Red now by design.
* feat(#2734): surface STATE.md commit-age on the statusline
Adds an opt-in `state ~N commits back` marker to the GSD-state segment,
consuming the `state_head` stamp and freshness contract landed by #2622.
A solo developer returning to a project reads "Phase 4, executing" in
STATE.md and acts on it, without noticing the codebase moved 40 commits
since that line was written. /gsd-health reports it as W024, but only if
you think to run it; the statusline is the surface you see without asking.
Fires at STATE_HEAD_ADVISORY_COMMITS (20), the same threshold W024 uses,
not at > 0: with commit_docs:true the commit carrying a STATE.md sync
advances HEAD by one, so > 0 would alarm permanently on a fresh project.
Costs exactly one bounded git subprocess per render and none when
disabled. `rev-list --left-right --count` answers ancestry and distance
together, and repo pinning is a filesystem check mirroring
projectOwnsItsRepo rather than a --show-toplevel compare, which is
unreliable on macOS /private/var and Windows 8.3 paths.
Every unresolvable input degrades to the tri-state unknown -- the marker
is absent, never a "fresh" claim the project cannot substantiate: a
malformed stamp, a root that does not own its .git, a sub_repos
workspace, history rewound past the stamp, or git being unavailable.
Also collapses statusline config resolution onto one resolveStatuslineOptions()
seam. runStatusline() and renderStatusline() duplicated it byte-for-byte;
one copy is what keeps a newly-added key from reaching only one of them.
* test(#2734): route the e2e spawn through the process seam and fix fixture leaks
Review findings from the two orthogonal passes:
- `bothEntryPointsResolveOptionsIdentically` spawned a child and substring-matched
its stdout to test a pure function. It now calls resolveStatuslineOptions()
directly — no subprocess, no text matching.
- `skipsFreshnessWorkWhenTodoTaskActive` genuinely needs a child (the !task gate
lives in runStatusline, which reads stdin), so it now spawns through
tests/helpers/process-seam.cjs and proves the negative with a filesystem fact:
the git shim appends to a marker file on every invocation, and the assertion is
that the marker never appears. Stronger than asserting text is missing, and it
drops the last stdout substring match in the block.
- Every fixture-creating test now registers `t.after(() => cleanup(dir))` instead
of a trailing cleanup(dir), which leaked the temp repo on assertion failure.
derivationAgreesWithStateModule reassigns `dir` across five fixtures, so it
binds each directory at scheduling time rather than cleaning only the last.
Also corrects markerCoexistsWithMilestoneComplete, which asserted the wrong
expectation rather than finding a code defect: `percent` drives the progress bar
too, so the milestone segment reads "v1.9 [##########] 100%". The marker appends
after it, which is what the test exists to prove.
CONTEXT.md's opt-in statusline key list was missing statusline.show_git as well
as the new key; both are now enumerated.
* docs(#2734): backfill changeset PR number (#3700)
---------
Co-authored-by: sim <sim@local>
* test(3582): failing-first cold-tree coverage and the seam drift lint
On a plugin-channel install the compiled gsd-core/bin/lib/*.cjs are legitimately
absent (ADR-457 build-at-publish; the npm package builds before publishing, a raw
tree materialization never does). gsd-tools.cjs calls ensureRuntimeBuild() before
requiring ./lib; no hook does, so the isolation guard's Cannot-find-module lands in
its fail-closed catch and is misreported as an unreadable dispatch-isolation
configuration, blocking every executor dispatch.
These tests fail on that: cold-tree runs of the isolation guard, statusline, cursor
guard and update worker, plus the seam's actionable build error surfacing instead of
the generic misreport.
Also adds the drift lint the acceptance criteria require, with a fixture proving it
CAN fail — a guard never shown to fail is worthless. It is red here by design: it
flags today's unfixed hooks, which is exactly the defect.
* fix(3582): route every hook's compiled-module require through the self-heal seam
RED proven at 5b174b0d: 11 failures — the cold-tree runs for the isolation guard,
cursor guard and update worker, the fail-closed-with-actionable-message assertion, and
the lint's own real-tree check.
The compiled runtime library is produced by build:lib and gitignored (ADR-457,
build-at-publish). The npm package builds before publishing; a plugin-marketplace or
git-clone install materializes the raw tree and never does, so on that channel those
modules are legitimately absent. The self-heal seam added by #2002 exists to heal exactly
this, and the CLI entrypoint already calls it — no hook did. The isolation guard's
Cannot-find-module therefore landed in its fail-closed catch and was reported as
'could not read or resolve dispatch-isolation configuration', so an ARTIFACT ABSENCE was
misdiagnosed as an unreadable project config and every executor dispatch was blocked.
All SEVEN affected files now call the seam before their first compiled require. The issue
named four; a scan found six; implementing it surfaced a seventh — the shared isolation
sentinel helper, used by BOTH guards, which requires two compiled modules itself and
would have defeated the guards' own fix on a genuinely cold tree. Same defect class, so
fixed here rather than left as a known-broken remainder.
Failure posture is deliberately split by hook kind:
- Gates (agent isolation guard, cursor subagent start) surface the seam's actionable
build error distinctly instead of swallowing it into the generic text, and stay
fail-closed — a genuinely unreadable project config still DENIES exactly as before.
- Cosmetic and detached hooks (statusline, update worker, update check, update banner)
DEGRADE rather than crash: the statusline draws on every render and the worker is a
detached process, so a build failure there must not take down the prompt.
The npm path is untouched: the seam's already-built fast path returns immediately, so
prebuilt installs pay nothing and behave bit-for-bit as before.
Adds a drift lint, wired into the CI lint chain, so the invariant is enforced rather than
remembered — without it the next hook to add a compiled require reintroduces the class
silently. It is proven able to fail: a fixture hook requiring a compiled module without
the seam is flagged, and one that uses the seam is not. Verified directly — on the
unfixed tree it named all seven offenders; with the fix it passes.
While writing the lint's comment stripper, a naive whole-text block-comment regex ate its
own fixture, because this repo's comments legitimately spell the compiled-lib glob whose
star-slash reads as a comment opener. Rewritten as a line-based scanner with a regression
test pinning that case.
* fix(3582): test the three untested seam call sites and assert typed reason codes
Two independent reviews converged on the same major gap: the fix wired the seam into
seven files but only four had cold-tree tests. The adversarial pass put it plainly —
deleting the shared isolation-sentinel helper's seam call would not have failed any test
in the diff. That file was my own addition beyond the issue's four, so it shipped
untested; that is now closed.
- Shared isolation-sentinel helper: its seam call is only reached when .planning is NOT
directly under cwd, and every existing cold-tree fixture puts it there, so the early
return always fired first. Now covered, and proven load-bearing by mutation: with the
call removed the spy records zero seam invocations and the test fails.
- update-check hook and update-banner hook: cold-tree tests added asserting the DEGRADED
VERDICT — the fallback cache filename, and silent suppression when the package name
degrades to null — rather than merely 'did not throw'. The banner hook previously had
no test file at all.
Standards violation fixed: two tests asserted on free-form prose via assert.match against
a JSON reason string, which CONTRIBUTING bans by name — its own BAD example is exactly
that. The ESLint rule only covers readFileSync/spawnSync text, so tooling did not catch
it. Both isolation guards now emit a machine-readable reason_code from a frozen enum,
following the repo's existing REASON convention, and the tests assert that instead. The
human-readable message is unchanged for operators; only the assertion target moved.
The duplicated degrade boilerplate across the three cosmetic hooks was deliberately NOT
extracted, and the reason is recorded at each site: both viable shapes — a
path-parameterized helper, or a ceremony-only wrapper — defeat the drift lint's per-file
literal co-occurrence check, so extracting would require the lint to special-case its own
helper. Triplication is the lesser evil while the lint stays a co-occurrence scan.
The lint's header now states what it does and does not catch (literal quoted requires
only; hooks/ scan root), so a future reader does not over-trust a guard that a
concatenated path or a require inside a non-hooks helper would evade.
* chore(3582): regenerate the committed install-tree fixtures
Adding a new shipped hook helper changed the install tree, and those fixtures are
committed-and-derived (regen:derived / gen:install-tree), so 12 'install tree — <runtime>'
tests failed on 541a1913. Regenerated rather than hand-edited.
The delta across all 15 runtime fixtures is exactly two lines — the new helper under both
its hooks/ and gsd-hooks/ install paths — and nothing else, so the regeneration pulled in
no unrelated drift.
This is the bookkeeping ripple a new file under hooks/ carries; it was not visible from
lint:ci, which passed both before and after.
* chore(3582): backfill changeset PR number (#3629)
---------
Co-authored-by: sim <sim@local>
* test(#2850): add failing-first tests for workstream statusline state
readGsdState only ever reads the flat .planning/STATE.md via a directory
walk-up; it has no path for .planning/workstreams/<ws>/STATE.md and never
consults GSD_WORKSTREAM or the stored active-workstream pointer, so the
GSD-state segment silently disappears in workstream mode. These tests
prove the RED before the fix lands.
Uses shared saveSessionEnv/restoreSessionEnv/clearSessionEnv helpers now
added to tests/helpers.cjs (single source of truth for the session-env-var
save/clear/restore pattern also used by tests/active-workstream-store.unit.test.cjs,
which is updated here to consume the same shared helpers instead of its own
local, already-diverged copy).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(#2850): resolve active workstream in the statusline
readGsdState only ever walked up looking for a flat .planning/STATE.md; it
had no branch for .planning/workstreams/<ws>/STATE.md and never consulted
GSD_WORKSTREAM or the stored active-workstream pointer, so the GSD-state
segment silently vanished in workstream-mode projects with no root
STATE.md (exit 0, no diagnostic).
Reuses the existing CLI>env>store resolution seam (resolveActiveWorkstream,
active-workstream-store.cts) and the existing mode-detection/path-building
seam (listAvailableWorkstreams/planningPaths, planning-workspace.cts)
rather than re-implementing either inline. When workstream mode is
detected but nothing resolves, readGsdState now returns a
{noActiveWorkstream:true} sentinel that formatGsdState/formatGsdStateCompact
render as "no active workstream" -- observable, never silent emptiness.
Flat-mode behavior and the case where a resolved workstream has no
STATE.md yet are both unchanged.
Adds active-workstream-store.cts's peekActiveWorkstream: a read-only
sibling of getActiveWorkstream. resolveActiveWorkstream's default store
lookup self-heals a stale/invalid pointer by deleting it
(adapter.clear()) -- correct for a command, but not for a renderer
invoked once per prompt, which must never mutate persistent, possibly
cross-session state as a side effect of drawing a screen. The statusline
now injects peekActiveWorkstream via resolveActiveWorkstream's own
getStored override, keeping the env>store precedence itself fully reused
while removing only the store tier's write side effect. This satisfies
the issue's AC4 ("the fix is purely additive to what's displayed").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(#2850): backfill changeset PR number to 3012
---------
Co-authored-by: sim <sim@local>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* 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>
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
- 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(#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>
* chore(#604): rename get-shit-done/ runtime directory to gsd-core/
Renames the installed runtime directory `get-shit-done/` to `gsd-core/` so the
on-disk name matches the package (`@opengsd/gsd-core`), repo, and binary
(`gsd-tools`). The npm package name and binary are unchanged; npx/npm consumers
are unaffected.
Mechanical (bulk, ~90% of the diff):
- `git mv get-shit-done gsd-core`
- Swept path/identifier references across the repo via
`perl -pe 's/get-shit-done(?!-\w)/gsd-core/g'`. The negative lookahead
preserves the five legitimate slug variants that are NOT the directory:
get-shit-done-{OLD,cc,classic,cli,redux} (old package/repo names).
- Build/manifest wiring: package.json (bin, files, coverage globs),
tsconfig.build.json (outDir), ~86 .gitignore build-output entries,
stryker.config.mjs, scan-ignore files, install.js path strings.
- Frozen (not rewritten): CHANGELOG.md history; translated docs
(README.<locale>.md and docs/{ja-JP,ko-KR,pt-BR,zh-CN}/).
New logic (review here):
- src/installer-migrations/003-rename-get-shit-done-to-gsd-core.cts: a proper
ADR-0008 installer migration. On upgrade it walks the legacy
`~/.claude/get-shit-done/` tree, classifies each file via the prior install
manifest, and emits remove-managed / backup-and-remove for managed files
while PRESERVING unknown user-added files. Symlink-safe (skips a symlinked
root and symlinked entries; bounds-checks every path under configDir). The
framework rolls back on install failure. Emptied dirs may remain (framework
has no recursive dir-removal primitive) — documented.
- scripts/lint-legacy-dir-name.cjs: CI regression guard forbidding the bare
`get-shit-done` directory token (split token to avoid self-match; case-
insensitive; `(?!-\w)` lookahead allows the slug variants; allowlists
CHANGELOG, translated docs, and `gsd-allow-legacy-name` marker lines).
Wired into the lint-tests CI job.
- Restored scripts/lint-package-identity-drift.cjs detection regexes (the
mechanical sweep had wrongly rewritten the old-name patterns it exists to
detect) and marked them as intentional legacy references.
- TDD tests for the migration and the guard; do.md slash-command guard regex
tightened so a `/gsd-core/bin` path segment is not mistaken for a command;
changeset + docs/installer-migrations.md row added.
Breaking: the installed runtime path moves `~/.claude/get-shit-done/` ->
`~/.claude/gsd-core/`. Migration 003 removes the stale legacy dir's managed
files (preserving user files) on upgrade. Users with custom hooks/configs
hardcoding the old path must update them.
Closes#604
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): unsweep pending changesets + allowlist injection-example docs
CI fixes for the rename PR:
- Do not sweep pending .changeset/*.md (ephemeral release-note fragments,
like CHANGELOG); reverted those body edits so 5 pre-existing malformed
fragments (missing type/pr) no longer enter the PR diff and trip docs-lint.
Allowlisted .changeset/ in the legacy-name guard accordingly.
- Allowlisted TEST-EXAMPLES.md and docs/explanation/security-model.md in
prompt-injection-scan.sh: they contain intentional injection examples /
security-model prose; the path-reference rewrites are kept.
CodeQL alerts on this PR are pre-existing (alert lines unchanged by this PR;
none in the new migration/guard) and are out of scope for the rename.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): resolve CodeQL alerts surfaced on this PR
The rename diff touched files carrying pre-existing CodeQL findings; per the
no-pre-existing-dismissal rule, fixing every surfaced alert rather than waving
them off. All behavior-preserving:
- scripts/ci-test-scope.cjs: build the config-path match from string
.includes() instead of a RegExp over an arg-derived value (js/regex-injection).
- src/profile-output.cts: escape backslashes before pipe-escaping desc/safeName
so the table-cell escape is complete (js/incomplete-sanitization).
- tests/{bug-2643,bug-2808,docs-parity-live-registry}: two-pass HTML-comment
strip so a bare/unclosed `<!--` cannot survive (js/incomplete-multi-character-sanitization).
- tests/inline-plan-threshold: drop the no-op `\s`->`\s` identity replace,
keep the meaningful POSIX-class conversion (js/identity-replacement).
Verified: build:lib green; the touched test files + ci-test-scope + profile-output
suites pass; lint:legacy-name clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): correctly resolve remaining CodeQL alerts (regex-injection + sanitization)
The prior commit's fixes for two alerts were ineffective:
- ci-test-scope.cjs js/regex-injection: the alert is the CLI-arg-derived `file`
reaching static regex `.test(file)` calls (not the config rule). Removed ALL
regex over file/t — startsWith/includes/=== string checks + an isWindowsHint
helper — so there is no regex sink for the tainted value.
- js/incomplete-multi-character-sanitization (3 test files): a single
`.replace(/<!--...-->/g,'')` can let `<!--` re-form. Replaced with a fixpoint
loop (replace until stable) plus a final bare-opener strip.
Verified: no regex over file/t remains; ci-test-scope + the 3 test suites pass;
lint:legacy-name clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): make ci-test-scope + comment-strippers regex-free to clear CodeQL
CodeQL flags the regex PATTERNS syntactically (regex-injection on the
--files arg split; incomplete-multi-character-sanitization on the <!--...-->
replace), so loop fixes do not satisfy it. Made these paths regex-free:
- ci-test-scope.cjs splitFiles: char-by-char separator tokenizer (no /[,\\s]+/).
- 3 test files: indexOf/slice HTML-comment stripper (no .replace(/<!--/)).
Behavior preserved; ci-test-scope + the 3 suites pass; guard clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): unblock security base64 scan on the large rename diff
The security job hit its 10m timeout: base64-scan.sh choked on the binary
test fixture tests/feat-3594-parser-property-style.test.cjs (embedded NUL/
non-UTF8 bytes -> thousands of bogus blobs + "ignored null byte" warnings),
and the ~800-file rename diff is slow to scan regardless.
- scripts/base64-scan.sh: skip binary-by-content files (grep -Iq .) — they
can't carry base64-obfuscated *text* and feeding NUL bytes through the
per-line scanner is pathologically slow. collect_files already filtered
binary *extensions*; this catches binary *content* in text extensions.
- .github/workflows/security-scan.yml: raise the security job timeout 10m->30m
to accommodate very large diffs (the scan itself is unchanged).
Verified locally: scan skips the fixture, 0 "ignored null byte" warnings,
0 findings, exit 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): sweep get-shit-done refs introduced by merging next
The branch was updated with next (#614/#384/#618 etc.), which reference the
get-shit-done/ dir (still named that on next). Swept the stale references in
the merged files to gsd-core so the rename stays consistent and lint:legacy-name
passes:
- commands/gsd/discuss-phase.md (runtime-launcher shim paths)
- src/core.cts (getAgentsDir layout comments)
- tests/bug-384-agents-runtime-aware.test.cjs (require path to runtime lib)
Verified: guard 0 violations; build green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): exclude gsd-core/ path segments from bug-3683 command cross-ref invariant
The #614 runtime-launcher shim added to discuss-phase.md references
`${_GSD_RUNTIME_ROOT}/gsd-core/bin/...`. bug-3683's REF_PATTERN excluded path-y
refs only via lookbehind, but `}` precedes `/gsd-core/` in the shim, so it
mis-read the directory path as a dangling `/gsd-core` command ref (same class as
the #604 bug-2954 fix). Added a trailing `(?![\w-]*\/)` so `/gsd-<x>/...` path
segments are not treated as slash-command references.
Verified locally on BOTH platforms before pushing:
- mac (node 26) full suite: 0 failures
- gsd-test-runner (linux, node22 image) full suite: 0 failures
- bug-3683 + bug-2954 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): lazily resolve findProjectRoot in gsd-tools (harden flaky CI)
CI intermittently failed state.test's gsd-tools subprocess with
"findProjectRoot is not a function" (flip-flopping across legs; not reproducible
on mac full suite, gsd-test linux full suite, test:unit, or state.test x8).
findProjectRoot is a re-export from core.cjs (sourced from project-root.cjs);
binding it via destructure at module-load can be undefined under a load-ordering
edge. Resolve it lazily at call time via a small wrapper so the lookup happens
after core.cjs is fully initialized.
Verified green on BOTH platforms before pushing:
- mac (node 26) full suite: 0 failures
- gsd-test-runner (linux, node22) full suite: 0 failures
- state.test.cjs: 106/106; gsd-tools loads cleanly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#604): allowlist verification-patterns.md placeholder examples in secret scan
The rename git-mv'd references/verification-patterns.md into gsd-core/, pulling
it into the secret-scan diff. It documents stub/placeholder RED-FLAG env-var
examples (illustrative Stripe test-key / database-URL / API-key placeholders) —
not real credentials. Added it to .secretscanignore with the strict annotation,
mirroring the existing gsd-core/workflows/plan-phase.md exception.
Verified locally: secret-scan-lint --strict OK; secret-scan --diff origin/next
exits 0 with 0 findings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(#607): rebuild get-shit-done-cc → gsd-core migration
Leftover get-shit-done-cc installs poisoned the shared update cache,
causing a permanent false "update available". Rebuild the migration so a
stale old install is both harmless and actively removed.
- Per-package update cache filename (gsd-update-check-<slug>.json) in the
shared ~/.cache/gsd dir, single-sourced via package-identity; writers
stamp package_name and readers reject foreign/absent lineage. Multi-
runtime visibility preserved (same shared dir + filename across runtimes).
- New get-shit-done/bin/lib/legacy-cleanup.cjs seam: detects code-file
references to the old package + the legacy fixed-name cache across home
runtime dirs; installer auto-cleans on every install; --dry-run previews
and mutates nothing. User hooks and dev-preferences are never touched.
- update.md cache-clear globs gsd-update-check*.json across ALL supported
runtimes (adds cursor/windsurf/augment/trae/qwen/hermes/codebuddy/cline).
- Fix worker MODULE_NOT_FOUND post-install (ship managed-hooks-registry.cjs
+ degrade gracefully) so the per-package cache is always written.
- Diataxis how-to: docs/cleanup-get-shit-done-cc.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(#607): add changeset for PR #611
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(#607): set USERPROFILE alongside HOME in dry-run install test for Windows
os.homedir() reads USERPROFILE on win32, so HOME-only isolation let the
spawned installer scan the real runner home on windows-latest. Set both.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
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>
Extract composeStatusline() helper from duplicated inline template logic in
runStatusline() and renderStatusline(). Both call sites now route through the
helper, which accepts a position param ('end' | 'front', default 'end').
- 'end' (default) preserves byte-identical output to v1.38.x and earlier
- 'front' renders ctx immediately after model name, before the first │
- Invalid values silently coerce to 'end' at runtime (belt-and-suspenders;
config-set rejects invalid values upfront via enum validator)
Adds statusline.context_position to VALID_CONFIG_KEYS in both CJS and TS
schemas, enum validator in config.cjs, docs row in CONFIGURATION.md,
and a changeset. Closes#2937.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(#2833): parseStateMd reads phase-lifecycle frontmatter fields
Extend parseStateMd() to parse 4 new STATE.md frontmatter fields that drive
the phase-lifecycle status-line proposed in #2833:
- active_phase : phase number when orchestrator is in-flight, null when idle
- next_action : recommended next command when idle
- next_phases : YAML flow array of phase numbers for next_action
- progress : nested block with completed_phases / total_phases / percent
All fields default to undefined when absent — formatGsdState() (next commit)
degrades gracefully so existing STATE.md files keep rendering as before.
YAML scope intentionally narrow:
- Only top-level scalar keys (status, milestone, active_phase, next_action)
- Only single-line flow array for next_phases ([...])
- progress block requires 2-space indent for nested keys
Block sequences (- item over multiple lines) and inline comments inside
nested blocks are NOT parsed — keeping the regex-based parser predictable.
Comments outside frontmatter or after the closing --- still work.
Tests: all 27 existing tests still pass (no behavior change for STATE.md
files that don't carry the new fields).
Refs #2833
* feat(#2833): formatGsdState renders phase-lifecycle scenes + opt-in progress bar
Extend formatGsdState() with three lifecycle scenes that activate when the
new STATE.md frontmatter fields (added in the previous commit) are present.
Also append an opt-in progress bar to the milestone segment when
progress.percent is available.
Scenes (first match wins; falls through to the existing path otherwise):
1. active_phase set → 'v2.0 [██░] X% · Phase 4.5 executing'
(status field carries the lifecycle stage:
discussing / planning / executing / verifying)
2. active_phase null + → 'v2.0 [██░] X% · next execute-phase 4.5'
next_action set (idle state — surfaces what the user should
run next without opening STATE.md)
3. percent=100 (or → 'v2.0 [██████████] 100% · milestone complete'
completed=total)
4. (default fallback) → 'v1.9 Code Quality · executing · ph (1/5)'
(existing rendering, byte-for-byte preserved
when none of the new fields are populated)
Backward compat is the design priority:
- STATE.md files without the new fields render identically to v1.38.x
- progress bar is opt-in (empty string when percent absent)
- Each new scene only activates when its specific fields are populated
A new helper renderProgressBar() generates the 10-segment bar that matches
the existing context meter style (so the two bars on the status-line are
visually consistent).
Tests: 27/27 existing tests still pass.
Refs #2833
* test(#2833): cover parseStateMd lifecycle fields + formatGsdState scenes
26 new tests organized in 5 describe blocks, modeled after the existing
enh-2538-statusline-last-command.test.cjs convention:
parseStateMd #2833 lifecycle fields (7 tests)
- reads active_phase / next_action / next_phases / progress.percent
- 'null' literal handled correctly
- YAML flow array parsing (1 item, multiple items)
- progress nested block (3 fields)
- absent fields return undefined
formatGsdState #2833 lifecycle scenes (6 tests)
- Scene 1: active_phase set → 'Phase X.Y <stage>'
- Scene 2: idle + next_action → 'next <action> <phases>' (1+ phases)
- Scene 3: percent=100 OR completed=total → 'milestone complete'
formatGsdState #2833 backward compatibility (4 tests) — CRITICAL
- Legacy STATE.md (no new fields) renders byte-for-byte unchanged
- Empty state, partial state, progress-bar-opt-in all preserved
progress bar rendering (6 tests)
- 0% / 50% / 100% / clamping / opt-in absence
formatGsdState #2833 scene priority (3 tests)
- active_phase wins over next_action when both populated
- next_action wins over fallback when active_phase null
- percent=100 wins over fallback even with phase set
Combined run: 53/53 tests pass (existing 27 + new 26).
Refs #2833
* docs(#2833): describe phase-lifecycle frontmatter fields and rendering scenes
Add docs/STATE-MD-LIFECYCLE.md as the canonical reference for the four new
STATE.md frontmatter fields and the four status-line rendering scenes
introduced by this proposal:
- Frontmatter field reference (active_phase / next_action / next_phases /
progress.percent) with type and population semantics
- Why progress.percent is intentionally the phase dimension and not the
plans dimension (plans dimension trends optimistic when future phases
are unplanned)
- The four rendering scenes including their priority order
- Stage-label convention for Scene 1 (discussing / planning / executing /
verifying matching the four phase orchestrators)
- Frontmatter parsing constraints — frontmatter must start at file head,
no comments inside nested blocks, next_phases is single-line flow only
- Backward-compatibility guarantee (locked in by the test suite)
- Cross-links to the foundation issue #1989 and the read-side issues
this proposal helps close
The document deliberately scopes itself to the read-side (what the hook
parses, what it renders). Write-side SDK and workflow changes that
auto-maintain the fields are out of scope for this PR so each piece can
be reviewed independently — see the issue thread for the full proposal.
Refs #2833
* test(#2833): simplify '0% renders 10 empty segments' assertion
Address CodeRabbit nitpick — drop the convoluted assert.equal that built
the expected value via .replace() and rely on the existing assert.ok
includes-check. The behavior under test is unchanged; the assertion is
just easier to read.
Refs #2884 review comment
Adds a `statusline.show_last_command` config toggle (default: false) that
appends ` │ last: /<cmd>` to the statusline, showing the most recently
invoked slash command in the current session.
The suffix is derived by tailing the active Claude Code transcript
(provided as transcript_path in the hook input) and extracting the last
<command-name> tag. Reads only the final 256 KiB to stay cheap per render.
Graceful degradation: missing transcript, no recorded command, unreadable
config, or parse errors all silently omit the suffix without breaking the
statusline.
Closes#2538
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 a user manually installs a dev branch where VERSION > npm latest,
gsd-check-update detects hooks as "stale" and the statusline showed
the red "⚠ stale hooks — run /gsd-update" message. Running /gsd-update
would incorrectly downgrade the dev install to the npm release.
Fix: detect dev install (cache.installed > cache.latest) in the
statusline and show an amber "⚠ dev install — re-run installer to sync
hooks" message instead, with /gsd-update reserved for normal upgrades.
Also expand the update.md workflow's installed > latest branch to
explain the situation and give the correct remediation command
(node bin/install.js --global --claude, not /gsd-update).
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
* fix: replace /gsd: command format with /gsd- skill format in all suggestions
All next-step suggestions shown to users were still using the old colon
format (/gsd:xxx) which cannot be copy-pasted as skills. Migrated all
occurrences across agents/, commands/, get-shit-done/, docs/, README files,
bin/install.js (hardcoded defaults for claude runtime), and
get-shit-done/bin/lib/*.cjs (generate-claude-md templates and error messages).
Updated tests to assert new hyphen format instead of old colon format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate remaining /gsd: format to /gsd- in hooks, workflows, and sdk
Addresses remaining user-facing occurrences missed in the initial migration:
- hooks/: fix 4 user-facing messages (pause-work, update, fast, quick)
and 2 comments in gsd-workflow-guard.js
- get-shit-done/workflows/: fix 21 Skill() literal calls that Claude
executes directly (installer does not transform workflow content)
- sdk/prompt-sanitizer.ts: update regex to strip /gsd- format in addition
to legacy /gsd: format; update JSDoc comment
- tests/: update autonomous-ui-steps, prompt-sanitizer to assert new format
Note: commands/gsd/*.md frontmatter (name: gsd:xxx) intentionally unchanged
— installer derives skillName from directory path, not the name field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plan-phase): preserve --chain flag in auto-advance sync and handle ui-phase gate in chain mode
Bug 1: step 15 sync-flag check only guarded against --auto, causing
_auto_chain_active to be cleared when plan-phase is invoked without
--auto in ARGUMENTS even though a --chain pipeline was active. Added
--chain to the guard condition, matching discuss-phase behaviour.
Bug 2: UI Design Contract gate (step 5.6) always exited the workflow
when UI-SPEC was missing, breaking the discuss --chain pipeline
silently. When _auto_chain_active is true, the gate now auto-invokes
gsd-ui-phase --auto via Skill() and continues to step 6 without
prompting. Manual invocations retain the existing AskUserQuestion flow.
* fix: remove <sub>/clear</sub> pattern and duplicate old-format command in discuss-phase.md
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- fix(#1572): phase complete now marks bold-wrapped plan checkboxes in ROADMAP.md
(`- [ ] **01-01**` format) by allowing optional `**` around plan IDs in the
planCheckboxPattern regex in both phase.cjs and roadmap.cjs
- fix(#1569): manager init no longer recommends 999.x (BACKLOG) phases as next
actions; add guard in cmdManagerInit that skips phases matching /^999(?:\.|$)/
- fix(#1568): add regression tests confirming init execute-phase respects
model_overrides for executor_model, including when resolve_model_ids is 'omit'
- fix(#1533): reject session_id values containing path traversal sequences
(../, /, \) in gsd-context-monitor and gsd-statusline before constructing
/tmp file paths; add security tests covering both hooks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two fixes for multi-runtime installations:
1. Update cache now writes to ~/.cache/gsd/ instead of the runtime-
specific config dir, preventing mismatches when check-update and
statusline resolve to different runtimes. Statusline reads from
shared path first with legacy fallback.
2. Stale hooks detection now checks configDir/hooks/ where hooks are
actually installed, not configDir/get-shit-done/hooks/ which does
not exist.
Closes#1421
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: hook version tracking, stale hook detection, and stdin timeout increase
- Add gsd-hook-version header to all hook files for version tracking (#1153)
- Install.js now stamps current version into hooks during installation
- gsd-check-update.js detects stale hooks by comparing version headers
- gsd-statusline.js shows warning when stale hooks are detected
- Increase context monitor stdin timeout from 3s to 10s (#1162)
- Set +x permission on hook files during installation (#1162)
Fixes#1153, #1162, #1161
* feat: add /gsd:session-report command for post-session summary generation
Adds a new command that generates SESSION_REPORT.md with:
- Work performed summary (phases touched, commits, files changed)
- Key outcomes and decisions made
- Active blockers and open items
- Estimated resource usage metrics
Reports are written to .planning/reports/ with date-stamped filenames.
Closes#1157
* test: update expected skill count from 39 to 40 for new session-report command
Hooks hardcode ~/.claude/ as the config directory, breaking setups
where Claude Code uses a custom config directory (e.g. multi-account
with CLAUDE_CONFIG_DIR=~/.claude-personal/). The update check hook
shows stale notifications and the statusline reads from wrong paths.
- gsd-check-update.js: check CLAUDE_CONFIG_DIR before filesystem scan
- gsd-statusline.js: use CLAUDE_CONFIG_DIR for todos and cache paths
Closes#870
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The context monitor and statusline hooks wait for stdin 'end' event
before processing. On some platforms (Windows/Git Bash), the stdin pipe
may not close cleanly, causing the script to hang until Claude Code's
hook timeout kills it — surfacing as "PostToolUse:Read hook error" after
every tool call. Add a 3-second timeout that exits silently if stdin
doesn't complete, preventing the noisy error messages.
Closes#775
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The statusline uses a hardcoded 80% scaling factor for context usage,
but Claude Code's actual autocompact buffer is 16.5% (usable context is
83.5%). This inflates the displayed percentage and causes the context
monitor's WARNING/CRITICAL thresholds to fire prematurely.
Replace the 80% scaling with proper normalization against the 16.5%
autocompact buffer. Adjust color thresholds to intuitive levels
(50/65/80% of usable context).
Closes#769
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude Code enforces an 80% context window limit as a safety mechanism.
The statusline was showing the raw percentage, which meant the bar never
reached 100% before Claude ran out of context.
This change scales the percentage so that:
- 0% real usage = 0% displayed
- 80% real usage (the actual limit) = 100% displayed
Color thresholds are adjusted accordingly to maintain the same visual
progression (green -> yellow -> orange -> red with skull).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
All GSD hooks now use gsd- prefix. Installer updated to clean up old
statusline.js file and hook registration from previous installs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>