Commit Graph

135 Commits

Author SHA1 Message Date
Tom Boucher
c7c2fe3c2b fix(#2587): resolve cursor hook workspace from workspace_roots, not cwd (#2680)
* fix(#2587): resolve cursor hook workspace from workspace_roots, not cwd

gsd-cursor-session-start.js and gsd-cursor-stop.js both resolved the project as
path.join(process.cwd(), '.planning', 'STATE.md'). Under the cursor-agent CLI,
hooks are invoked with cwd set to the Cursor config dir (~/.cursor), not the
workspace — so the lookup always missed. sessionStart could only ever emit the
"no .planning/ workflow found" nudge and stop's verify-work reminder could never
fire, even with .planning/STATE.md sitting in the workspace. Slash commands were
unaffected, which is why only the hook layer looked blind.

Both hooks already buffered stdin into `raw` and never parsed it; the payload's
workspace_roots carries the real path.

Multi-root was left open in the report ("first root vs any root"). Resolved
forward: prefer the first root that actually carries .planning/STATE.md, so a
workspace whose GSD project is not the first root still resolves — strictly
better than first-root-only and identical to it in the single-root CLI case.
Falls back to roots[0], then to cwd, keeping IDE behavior unchanged if the IDE
ever invokes hooks from the workspace.

The resolver is duplicated verbatim across the two scripts rather than shared via
hooks/lib/: these hooks ship standalone, and a new hooks/lib/ file must be
registered in the GENERATED installer's GSD_HOOK_LIB_FILES allowlist — the
installer-omits-shipped-file class that yields MODULE_NOT_FOUND at runtime. Per
CLAUDE.md "Generative Fix Divergence", the duplication carries a parity assertion
so the copies cannot drift.

Failing-first, demonstrated by direct invocation with cwd != workspace:
  pre-fix  sessionStart -> "no .planning/ workflow found"   stop -> {}
  post-fix sessionStart -> ".planning/STATE.md is present"  stop -> reminder

tests/fix-2587-cursor-hook-workspace-roots.test.cjs spawns the real scripts as
child processes with a cwd lacking .planning/ and workspace_roots pointing at it.
Boundary coverage on the roots array (0 / 1 / 2 entries), plus malformed-JSON
fail-open, junk-entry filtering, the parity assertion, and a guard that neither
script resolves .planning from cwd again.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* fix(#2587): extend workspace_roots fix to subagentStart; keep cwd a candidate

Three findings from the isolated review, all fixed.

1. MISSED SITE (high). gsd-cursor-subagent-start.js carried the identical
   defect at line 43 — its own header documents workspace_roots in the input
   schema, but it resolved .planning/ from process.cwd() anyway. Under the
   cursor-agent CLI that meant every Cursor subagent (planner, executor,
   verifier) started with "no .planning/ workflow found" and no phase context.
   The report named only sessionStart and stop; the defect class was wider.
   Verified pre-fix vs post-fix by direct invocation with cwd != workspace.

2. SEMANTIC NARROWING (medium). The first cut searched only workspace_roots and
   fell back to cwd solely when the array was EMPTY. So when roots were supplied
   but none carried .planning/ while cwd did, the hook reported absent — where
   the pre-fix code, which always used cwd, reported present. That contradicted
   the fallback's own stated intent of preserving IDE behavior. cwd is now a
   CANDIDATE in the search (`[...roots, process.cwd()]`), so the fix is a strict
   superset of both the old behavior and the CLI fix, never a narrowing.

3. STALE GOLDEN FIXTURES (high, would have failed CI). The golden-install-parity
   fixtures store a content hash per installed file; these three hooks appear in
   13 of the 19 runtime fixtures. Regenerated via `npm run gen:golden` — the
   diff is exactly the three hook hashes in exactly those 13 runtimes.

Tests extended: subagentStart resolution via workspace_roots; the stop hook's
absent branch (previously only session-start's was covered); an explicit
regression guard that a project at cwd is still found when roots miss; parity now
asserts all THREE copies byte-identical; and the cwd guard sweeps the whole
RESOLVING_HOOKS list so a future hook in this family cannot be left on cwd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* refactor(#2587): extract cursor workspace resolution to a shared hooks/lib module

The duplicate-plus-parity-test approach was the wrong call. The reported issue
named two hooks; a third (subagentStart) had the identical defect. That is the
signature of a systemic problem, and three copies of a resolver guarded by a
parity assertion is a divergence risk maintained by hand rather than a fix.

hooks/lib/cursor-workspace.js is now the single implementation. All three Cursor
hooks require it; none defines a local copy. Divergence is prevented
structurally instead of by asserting three copies stay byte-identical.

The reason duplication looked necessary was real, and is fixed properly here
rather than worked around: Cursor sets hostBehaviors.skipSharedHooksInstall
(#2089), so it never reaches the installer's bulk hooks/lib copy — it was the
ONE runtime shipping these hooks WITHOUT hooks/lib (verified against all 19
golden fixtures: cursor had the hook scripts, no lib). A naive require would
have thrown MODULE_NOT_FOUND at load, BEFORE each hook's own try/catch, wedging
every session on precisely the runtime this bug is about.

writeCursorHooksJson (src/runtime-hooks-surface.cts) now stages the hooks/lib
helpers the staged scripts actually require, discovered by scanning their
require('./lib/…') calls rather than a hardcoded name — so a future helper
cannot be silently omitted. This is narrower than flipping
skipSharedHooksInstall, which would wrongly pull in every shared hook.
cursor-workspace.js is also added to GSD_HOOK_LIB_FILES so uninstall and the
manifest manage it for the runtimes that do receive hooks/lib.

Verified against a REAL install (runMinimalInstall, cursor/global): the helper
is staged, and all three INSTALLED hooks resolve the workspace end-to-end from a
cwd that is not the project.

Also closes the review gap that the stop hook was excluded from the
cwd-candidate regression loop — it now sweeps RESOLVING_HOOKS. The byte-parity
test is replaced by a structural guard (every hook requires the shared module,
none redefines it) plus a new install test asserting the helper is staged and
the installed hook actually loads against it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* fix(#2587): fail loud on a missing hook lib source; drop unsubstituted version marker

Two findings from the installer-focused review.

H1 — the staging step's `if (!fs.existsSync(libSrc)) continue;` silently defeated
the very guarantee it was added for. Reproduced: delete hooks/lib/cursor-workspace.js
from source, run the cursor install — it exits 0, prints "Done!", and ships the
three hook scripts with an EMPTY hooks/lib/. The installed hook then throws
`Cannot find module './lib/cursor-workspace.js'` at load, before its own
try/catch, wedging every session — and nothing surfaces until a user hits it.
The scan protected against a required-but-UNLISTED helper while leaving
required-but-MISSING wide open (typo, bad rebase, an accidental delete).
It now throws: a missing helper source is a packaging bug and aborts the install.

M1 — hooks/lib/cursor-workspace.js carried a `gsd-hook-version: <placeholder>`
marker that NOTHING substitutes: copyLibDir stamps .sh files only, and
writeCursorHooksJson's staging applies just the colon-to-dash rewrite. Verified
the literal was reaching disk on both the bulk (--claude) and Cursor
(--cursor) paths. hooks/lib/git-cmd.js — the only pre-existing hooks/lib/*.js —
carries no such marker, so this was newly introduced, not inherited. Marker
removed, matching that precedent, with a note on why. (The explanatory comment
deliberately does not spell the token out, or it would reintroduce the literal.)

M2 — the require-scan regex demanded the exact compact form, so
`require( "./lib/x.js" )` would silently fail to stage its helper and compound
H1. Now tolerant of interior whitespace and either quote style.

Regression test added for H1 — the reviewer confirmed the invariant had zero
coverage repo-wide: a source tree carrying the hooks but no hooks/lib/ must make
writeCursorHooksJson throw rather than produce a broken install.

Re-verified end to end: the missing-source case throws, no unsubstituted literal
ships, and the installed hook still resolves the workspace from a foreign cwd.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015TCwhbMuY37DzRMCfzTABJ

* chore(#2587): backfill changeset pr number (#2680)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:57:58 -04:00
Tom Boucher
77bf21b3a6 fix(#1995): widen worktree branch regex to accept agent-<id> namespace (#2548)
* test(#1995): regression test for agent-<id> branch namespace

Add failing-first tests proving that normalizeCleanupManifestEntry and
planWorktreeRecordAgent reject Claude Code's current agent-<id> isolation
branches (only worktree-agent-<id> is accepted). Boundary tests cover both
namespaces plus rejection cases.

* fix(#1995): widen worktree branch regex to accept agent-<id> namespace

Claude Code's isolation="worktree" branch naming changed from
worktree-agent-<id> to agent-<id>. Widen the regex in all 7 locations
from ^worktree-agent-[A-Za-z0-9._/-]+$ to ^(worktree-)?agent-[A-Za-z0-9._/-]+$
so both namespaces are accepted. Introduce a shared WORKTREE_AGENT_BRANCH_RE
constant in src/worktree-safety.cts to prevent future drift.

Closes #1995

* fix(#1995): update workflow guards, test assertions, and baselines

Widen the branch-check regex in execute-phase.md and execute-plan.md.
Update all test assertions that checked for ^worktree-agent- to expect
the widened ^(worktree-)?agent- pattern. Regenerate golden-install-parity
fixtures, agent-size-baseline, and workflow-size-baseline.

Closes #1995

* fix(#1995): update extractCwdGuardBash sanity check for widened regex

The e2e test's sanity check verified the extracted bash block contained
'worktree-agent-'. After widening to '(worktree-)?agent-', update the
check to match the new pattern.

* fix(#1995): widen missed workflow-guard branch check + changeset + lint fixes

- hooks/gsd-workflow-guard.js: widen startsWith('worktree-agent-') to
  /^(worktree-)?agent-/ regex — same defect class, was missed in prior commit
- tests/worktree.test.cjs: fix indentation regression from prior edit
- Add .changeset/1995-worktree-agent-branch-namespace.md (pr:0 placeholder)

Found by orthogonal code review (Step 4).

* fix(#1995): regenerate golden + size baselines for workflow-guard change

* docs(#1995): backfill changeset PR number (2548)
2026-07-23 07:36:53 -04:00
Tom Boucher
7e905aa137 feat(#2505): Phase 0 — Kimi PreToolUse guard vocabulary normalization (precondition; carries PR #2326 forward) (#2518)
* fix(#2304): normalize Kimi tool vocabulary in PreToolUse guard payload checks

The Kimi [[hooks]] registrations translate the matcher to Kimi's tool
vocabulary (WriteFile|StrReplaceFile) but the guard scripts early-exit
unless the payload's tool_name is a Claude name (Write/Edit/MultiEdit),
so every guard was dormant on Kimi: the matcher fired, the script saw
WriteFile, and exit(0)'d.

Normalize the payload's tool_name at the top of each guard
(WriteFile -> Write, StrReplaceFile -> Edit; bare or module-qualified
kimi_cli.tools.file:* forms) before the check. Inlined per guard rather
than a hooks/lib/ helper because hook scripts are staged as standalone
files on every hook surface, and a sibling require is a staging
dependency that can fail silently.

Regression tests pipe Kimi-vocabulary payloads at each guard and assert
it engages (typed fields: exit status, decision, hookSpecificOutput) —
verified red against the pre-fix scripts, green after.

* fix(#2304): normalize Kimi tool_input fields and route block reasons to stderr

Cross-AI review of the initial fix, verified against kimi-cli source,
found the tool_name normalization alone leaves the guards dormant on a
real Kimi runtime: kimi-cli forwards tool_input verbatim
(src/kimi_cli/hooks/events.py), and its tool schemas
(src/kimi_cli/tools/file/{write,replace}.py) use path/content and
edit.old/edit.new (single Edit or list) — not Claude's
file_path/old_string/new_string. The guards read file_path, got '',
and exited 0 past the now-open tool_name gate.

Extend the per-guard normalization to the payload fields
(path -> file_path, edit -> old_string/new_string with list flattening),
and write the worktree guard's block reason to stderr as well as the
stdout JSON — Kimi feeds stderr, not stdout, back to the model on
exit 2 (docs/en/customization/hooks.md exit-code table).

Regression tests rewritten to Kimi's actual payload shapes (plus an
edit-list case and a stderr-reason assertion) — verified red against
the name-only fix, green after.

* fix(#2304): join all edit[] entries into old_string, matching new_string

Review nit on #2326: old_string took only edits[0].old while new_string
joined the whole list. Symmetric join removes the latent trap for any
future consumer sizing before/after content (e.g. the #2255 write guard).

* fix(#2304): normalize Kimi ReadFile vocabulary in read-injection scanner

Review Major 2 on #2326: gsd-read-injection-scanner.js had the identical
dormancy — its Kimi matcher fires on 'ReadFile' but the SCANNED_TOOLS
check only knew 'Read', so injected content in read files was never
flagged on Kimi installs.

Folds the same inlined normalization block into the scanner and extends
the shared KIMI_TOOL_NAMES map with ReadFile:'Read' in all four copies so
they stay byte-identical. Harmless in the three write guards: a
normalized 'Read' falls out of their Write/Edit allowlist exactly as the
unmapped name did. Field mapping verified against kimi-cli upstream
(src/kimi_cli/tools/file/read.py Params.path); the existing
path->file_path copy covers the scanner's file_path read.

* test(#2304): parity test binding the four inlined Kimi normalization copies

Review Major 1 on #2326: KIMI_TOOL_NAMES + normalizeKimiPayload is
deliberately inlined in four hook scripts (staging-dependency rationale,
unchanged), with the inverse table in bin/install.js — five
hand-maintained surfaces and nothing binding them.

Static binding, zero runtime coupling:
- the four inlined blocks must be byte-identical;
- each guard-map entry must be the value-inverse of
  convertKimiToolName() for its Claude name;
- every guard-relevant Claude tool (Write/Edit/MultiEdit/Read) must have
  a reverse entry — a vocabulary rename or extension that updates the
  installer without updating the guards now fails in CI instead of
  leaving a guard silently dormant (the #2304 recurrence door).

Negative-controlled: diverging one copy or dropping a map entry fails
the suite against the fixed code.

* test(#2304): regenerate golden parity fixtures for guard hook changes

CI red on #2326: all 10 golden-parity failures were the staged guard
hooks drifting from their fixtures. Regenerated with npm run gen:golden
(after npm run build) under throwaway HOME/CLAUDE_CONFIG_DIR; diff
verified to change exactly the four PR-touched guard entries per
surface, nothing else.

* test(#2304): regression tests for Kimi ReadFile engaging the scanner

Mirrors the per-guard Kimi vocabulary tests the PR added for the three
write guards: bare and module-qualified ReadFile produce the advisory,
path exclusions still apply post-normalization, unknown Kimi names stay
fail-open. Negative-controlled against the pre-fold scanner (the two
positive cases fail there; exclusion/fall-through correctly pass on
both sides).

* fix(#2304): normalize Kimi Shell vocabulary in workflow guard

Withdraws the disclosed out-of-scope split: verification showed the
Bash->Shell case needs NO different mapping — kimi-cli's Shell.Params
names its field `command` (src/kimi_cli/tools/shell/__init__.py), same
as Claude's Bash — and the guard's write branch (Write/Edit/MultiEdit
allowlist) was ALSO dormant on Kimi under its Shell|WriteFile|
StrReplaceFile matcher. Same defect class as the other four hooks.

Folds the identical inlined block into gsd-workflow-guard.js and
extends the shared map with Shell:'Bash' in all five copies (harmless
outside the workflow guard: a normalized Bash falls out of the other
guards' checks as before). Parity test now binds five copies and adds
Bash to the dormancy alarm. New workflow-guard test file exercises the
observable block (force-add on a worktree-agent branch): Shell bare and
module-qualified block with WORKTREE_AGENT_FORCE_ADD_FORBIDDEN, benign
Shell passes, Claude Bash unchanged — negative-controlled against the
pre-fold guard (the two Kimi cases fail there). Golden parity fixtures
regenerated; diff verified to change exactly the five guard entries per
surface.

* fix(#2304): map Kimi tool_output and route workflow-guard block to stderr

Third-party review (cross-AI verifier) caught two gaps in the revision:

1. Kimi PostToolUse events carry `tool_output`, not `tool_response`
   (kimi-cli src/kimi_cli/hooks/events.py post_tool_use()), so the
   read-injection scanner — which reads data.tool_response — was STILL
   dormant on real Kimi payloads; the earlier tests passed because they
   sent Claude-shaped payloads. The shared normalization block now maps
   tool_output -> tool_response (inert in PreToolUse guards, where the
   field is absent), and the scanner's Kimi tests send the real shape.

2. The workflow guard's force-add block wrote its reason to stdout only.
   Kimi's exit-2 protocol feeds stderr back to the model — the exact
   fix this PR already applied to the other blocking guard — so the
   newly-awakened block would have been a silent denial. Reason now
   also routed to stderr, asserted in the test.

Also: the scanner's "unknown name" test now uses a genuinely unmapped
name (FetchURL) — Shell stopped qualifying when it entered the map —
and the workflow guard's write branch (WriteFile advisory,
StrReplaceFile .planning pass) gains behavioral coverage. All five
copies stay byte-identical (parity test green); golden fixtures
regenerated, diff verified to the five guard entries per surface.
Negative-controlled: 3 new assertions fail against the pre-fix hooks.

* docs(#2304): update changeset to cover the full five-guard fix

Review round 2 (2026-07-18) flagged the changeset as stale: it was
written for the first commit and still described only the three guards
named in the issue. The shipped diff grew to five guards plus two
payload dimensions the original body never mentioned. The body now
names gsd-read-injection-scanner and gsd-workflow-guard, the ReadFile
and Shell vocabulary entries, the tool_output -> tool_response mapping,
and the workflow guard's stderr block-reason routing.

* test(#2304): regenerate kilo golden fixture after #2305 landed on next

The branch's fixture sweep predates 50efae13 (fix(#2305), PR #2327),
which made Kilo ship the five shared guard hooks. Rebased onto next and
re-ran the full generator sweep (gen:golden, size:baseline, and the
four registry/contract generators); the only delta across all of them
is kilo.json's five guard-hook hashes, matching this PR's hook edits.

* fix(#2304): fold Kimi normalization into the two shell hooks

The 2026-07-19 review found the last two guards with the #2304 dormancy:

- hooks/gsd-graphify-update.sh gated on tool_name == "Bash" but is
  registered on Kimi with matcher 'Shell' — Gate 1 never matched and the
  auto-rebuild was silently dormant. kimi-cli's Shell.Params names its
  field `command` (src/kimi_cli/tools/shell/__init__.py), same as Claude
  Bash, so only the name needs mapping: strip the module-path prefix,
  map Shell -> Bash.
- hooks/gsd-phase-boundary.sh read only tool_input.file_path, but Kimi's
  file tools name the field `path` (src/kimi_cli/tools/file/write.py +
  replace.py) — the hook read '' and .planning/ writes went undetected.
  Falls back to tool_input.path when file_path is absent, mirroring
  normalizeKimiPayload's precedence in the JS guards.

The normalization is reimplemented in shell — a byte-identity assertion
cannot span the JS<->shell boundary, so the parity test gains a
shell-guard vocabulary block that pins both scripts' mapping facts to
convertKimiToolName's live vocabulary instead of faking a byte binding.
Behavior is covered by negative-controlled tests beside each hook's
existing suite (verified red against the pre-fix scripts): Kimi Shell
dispatch (bare + module-qualified) with a WriteFile negative control in
graphify-auto-update.slow.test.cjs, and Kimi path detection, file_path
precedence, and a non-.planning negative control in hooks-opt-in.test.cjs.

Changeset updated to name all seven guards; golden install-parity
fixtures regenerated (diff is exactly the two hook entries per runtime;
size baselines unchanged).

* fix(#2304): use a Map for KIMI_TOOL_NAMES so prototype keys cannot pass the guard fall-through

A bare bracket lookup on an object literal resolves 'constructor',
'__proto__', 'toString', 'valueOf' and 'hasOwnProperty' through
Object.prototype to truthy functions/objects, so `if (!mapped)` failed
to short-circuit and data.tool_name was assigned a non-string. Map.get
returns undefined for those keys — the same shape the repo already uses
in canonicalizeRuntimeName (src/runtime-name-policy.cts). Applied
identically to all five inlined copies (review M1, PR #2326).

No new bypass class: unrecognized strings already fail open by design;
this fixes the lookup being wrong, not the posture.

* test(#2304): enumerate normalized guards by scanning hooks/, not a hardcoded list

The parity test's file list was a literal five-entry array — a sixth guard
with its own copy-pasted normalization block would be silently uncovered,
the exact divergence mode the test exists to prevent (review M2). Now the
list is a scan of hooks/*.js for the KIMI_TOOL_NAMES marker, with a floor
assertion so a scan that finds nothing fails instead of passing vacuously.
Also parses the Map declaration introduced by the M1 fix, and carries the
allow-test-rule annotation documenting the source-text scanning (review m4).

* test(#2304): parse hook JSON output instead of substring-matching raw stdout

workflow-guard.test.cjs asserted on unparsed stdout while read-guard.test.cjs
in the same PR parses the JSON envelope first — match the better pattern at
all four assertion sites (review m5).

* test(#2304): regenerate golden parity fixtures after Map conversion in the five guards

* docs(#2304): reset changeset pr:0 placeholder for Phase 0 PR (#2507)

The closed PR #2326's changeset carried pr:2326. Phase 0 of epic #2505
re-lands this fix on a fresh branch; the pr: field will be backfilled
to the real Phase 0 PR number immediately after gh pr create returns.

* docs(changeset): backfill PR #2518 for Phase 0 (#2507)

---------

Co-authored-by: 0xdhx <darkhawkx@gmail.com>
2026-07-21 23:42:02 -04:00
Tom Boucher
5ea3401d4f fix(#2289): context-monitor emits injection envelope only for supported events (#2324)
* fix(#2289): context-monitor emits injection envelope only for supported events

gsd-context-monitor is wired to Codex Stop/SubagentStart/SubagentStop/PreCompact
(#772), but it emitted a hookSpecificOutput.additionalContext envelope for every
event. Codex's Stop schema rejects that shape ("hook returned invalid stop hook
JSON output") exactly when context is low.

Use a positive allowlist: emit only for context-injection events (PostToolUse,
AfterTool, and the pre-existing Gemini missing-name fallback); exit 0 silently
for Stop and every other event. Debounce and critical-session bookkeeping still
run on silenced events. Behavioral regression tests drive the real hook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(#2289): backfill PR number 2324 into changeset

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(#2285): de-flake per-plan use_worktree property test on duplicate briefs

The fast-check property in claude-orchestration.test.cjs located each plan's
agent() call via indexOf on the brief, but only plan IDs were unique — briefs
could collide (fast-check shrinks toward short strings). On a colliding seed the
lookup found the first duplicate's line and misattributed its isolation, failing
"use_worktree:true must carry isolation" intermittently (surfaced on macOS CI).
emitWorkflowScript is correct for duplicate briefs (verified); suffix the unique
id onto each agent() label so the test probe is unambiguous.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 00:56:07 -04:00
Cody Anderson
20ff405cb3 feat(#2162): opt-in compact GSD-state format for the statusline (#2175)
* 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
2026-07-14 21:09:24 -04:00
Cody Anderson
eec9efc351 feat(#2160): collapse verbose '(1M context)' model suffix to compact (1M) badge (#2173)
* 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>
2026-07-14 20:37:43 -04:00
Cody Anderson
85a30d7036 fix(#2163): set windowsHide on the git status spawn
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
2026-07-13 12:19:20 -06:00
Cody Anderson
73039d3717 fix(#2163): round-2 review fixes — deterministic fail-soft injection tests
- 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
2026-07-13 11:33:43 -06:00
Cody Anderson
7a6564ed8a fix(#2163): review fixes — changeset format, explicit maxBuffer, hoisted require
- 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
2026-07-13 11:33:32 -06:00
Cody Anderson
d6672ff926 feat(#2163): opt-in git branch/status segment in the statusline
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
2026-07-13 11:33:32 -06:00
Cody Anderson
ad7111e50b feat(#2161): opt-in absolute token count on the statusline context meter (#2174)
* 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>
2026-07-13 13:25:47 -04:00
Tom Boucher
bd613566cb feat(#2100): drive Windsurf through the EoS descriptor + wire Cascade's blocking hook bus (ADR-1239)
Fold all 10 residual isWindsurf branches in bin/install.js onto descriptor-driven
hostBehaviors (byte-parity — no fold changes any install output):
- 2 dead destructures dropped (uninstall, finishInstall); the dead
  `else if (isWindsurf)` legacy agent-loop arm removed (windsurf ∈
  _DESCRIPTOR_AGENTS_RUNTIMES → unreachable).
- skipSharedHooksInstall:true folds the two `!isWindsurf` shared-hooks exclusions.
- legacyDevinSkillsCleanup:true folds the `.devin`→`.windsurf` one-time cleanup gate.
- installsCommandBodiesForWorkflowDelegation:true folds the #1629 command-body copy
  (workflow-delegation target — load-bearing; local-install verified intact).
- verificationStyle:"windsurf-workflows" folds the workflow-count report.
- Corrected stale _LEGACY_SCAN_SUBDIR_NAMES + hooks-json manifest comments (cursor + windsurf).
Zero live runtime==='windsurf'/isWindsurf branches remain across bin/install.js,
install-engine.cts, surface.cts, runtime-artifact-conversion.cts (AC2 guard scans all four).

UPGRADE (Cascade hook bus): wire GSD's write/command safety guards into Windsurf's
native hook bus. New hooksSurface 'windsurf-hooks-json' (VALID_HOOKS_SURFACES 7→8, GATE A
profile-marker-only allowlist, the HooksSurface union) + writeWindsurfHooksJson
(Cursor-templated, Cascade's flat {hooks:{<event>:[{command}]}} shape) writing
.windsurf/hooks.json with two BLOCKING pre-hooks:
- pre_write_code → gsd-windsurf-pre-write.js: blocks writes to a file outside the
  active git worktree / into .git internals.
- pre_run_command → gsd-windsurf-pre-command.js: conservative destructive-command
  deny-list (rm -rf of root/home incl. sudo/env/path-prefixed forms; fork bombs;
  force-push refspec forms — HEAD:main, +main, --force/-f — to main/master/next).
Both use Cascade's protocol (stdin JSON, exit 2 + stderr to block, exit 0 to allow,
fail-open on error/timeout). Tokenize-based classifier (no catastrophic-backtracking regex;
4096-char cap) with the fail-closed false-positives fixed post-review.

The 4 advisory GSD guards + pre_mcp_tool_use + 5 post_* logging events are deliberately
NOT wired: Cascade has no context-injection channel for advisory hooks and GSD has no MCP
guard — porting them would be non-functional padding (documented; codebuddy #2098 / copilot
#2099 faithful-subset precedent). extendedHookEvents stays [].

Golden: the 2 guard scripts ship in the shared hook bundle (HOOKS_TO_COPY + the shared
managed-hooks-registry), exactly like cursor's 6 gsd-cursor-*.js scripts — so the 8
shared-bundle runtimes' fixtures gain the 2 inert windsurf scripts + the registry hash
(functionally inert for non-windsurf; the established cursor pattern). No install-output
change beyond that (the folds are byte-parity; skip-bundle runtimes untouched). New scripts
registered in managed-hooks-registry + build-hooks + INVENTORY. Tests: declarative-reference-
windsurf (adapter/axes/fail-closed + AC2 guard) + windsurf-hooks-bridge (live exit-2 blocking
+ allow/fail-open + ReDoS-bound + writer/reconcile/remove idempotency); VALID_HOOKS_SURFACES
pin updated to 8. Matrix hookBus delta + changeset (Changed). capability-registry regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 16:04:24 -04:00
Tom Boucher
c25b212c62 revert: restore gsd-cursor-pre-tool.js dead imports (golden parity)
Reverts the LOW-severity dead-import removal (require('fs')/require('path'))
that changed the file hash and broke 9 golden-install-parity fixtures. The
golden test computes per-runtime hashes of installed hook files; regenerating
all 9 fixtures for a cosmetic cleanup is disproportionate. Dead imports are
harmless (Node caches built-in requires) — noted as a follow-up nit.
2026-07-09 13:39:24 -04:00
Tom Boucher
45f3a2a5f9 fix(#2089): wire adapter into install path + address all review findings
MEDIUM fixes (code review):
- Wire resolveManagedHookEvents + resolveHookScripts + buildHookBusEntries
  from imperative-hook-bus.cts into writeCursorHooksJson — the install path
  is now truly descriptor-driven (reads hostBehaviors.managedHookEvents),
  not a hardcoded constant that happens to match the descriptor. bin/install.js
  passes the descriptor list via opts.managedHookEvents.
- buildHookBusEntries is now consumed (was dead code); entry-building is no
  longer duplicated inline.
- Remove try/finally from cursor-hook-bus-upgrade.test.cjs test bodies
  (violated CONTRIBUTING.md L342; redundant with t.after cleanup).

LOW fixes:
- Remove dead require('fs')/require('path') from gsd-cursor-pre-tool.js
- Fix resolveManagedHookEvents docstring (all-invalid fallback behavior)
- Add src/runtime-hooks-surface.cts to the AC2 source-guard file list

Security review: no CRITICAL/HIGH/MEDIUM findings (3 LOW are pre-existing
#777 baseline patterns, not regressions).
2026-07-09 13:17:04 -04:00
Tom Boucher
24896ddac7 fix(#2089): register 4 new cursor hook scripts in build + managed-hooks whitelists 2026-07-09 00:58:09 -04:00
Tom Boucher
b0d985ccb3 feat(#2089): migrate cursor onto imperative adapter + hook-bus/dispatch upgrades 2026-07-09 00:21:39 -04:00
Tom Boucher
2d314c3a28 fix(#1772): read full multi-line command in graphify-update hook Gate 2 (#1815)
* fix(#1772): read full multi-line command in graphify-update hook Gate 2

The PostToolUse hook joined tool_name + newline + tool_input.command and
extracted the command with sed -n '2p' — line 2 only. Agent runtimes
(Claude Code's Bash tool among them) routinely emit HEAD-advancing commits
as multi-line scripts ('cd /path', then 'git add', then 'git commit …'), so
line 2 is the 'cd', Gate 2's *"git commit"* match failed, and the rebuild
silently no-op'd on real commits despite graphify.auto_update: true.

Capture line 2 through EOF (sed -n '2,$p') so the case glob sees the full
multi-line command string. Single-line behavior is unchanged (the match
only widens); non-HEAD-advancing multi-line commands still no-op cleanly.

Regression tests cover multi-line commit/merge/pull dispatch plus a
multi-line no-op no-regression guard.

* docs(#1772): add changeset fragment for graphify-update multi-line fix

* test(#1772): regenerate golden-install-parity fixtures for hook change

gsd-graphify-update.sh ships to 9 graphify-aware runtimes; widening the
sed range (2p -> 2,$p) shifts its shipped hash. Recapture the 9 affected
fixtures via UPDATE_GOLDEN=1 — each changes exactly one line (the hook hash).
2026-06-28 22:42:23 -04:00
Alex V.
a63684c222 enhance(#1577): WebFetch/WebSearch injection isolation + opt-in blocking (#1585)
* fix(#1577): isolate WebFetch/WebSearch ingress + opt-in injection blocking

Split A of #1573 (security-critical). Scans WebFetch/WebSearch output (the
largest untrusted channel) in gsd-read-injection-scanner; shared
untrusted-input-boundary reference @-included by the 8 ingest agents
(randomized per-wrap delimiters, in-prompt self-scan guard, task-anchoring);
opt-in security.injection_blocking (default advisory — non-breaking).

arXiv: 2506.05739 (PPA), 2507.15219 (PromptArmor), 2504.20472 (Referencing), 2503.00061 (defense-in-depth).

* fix(#1577): address review — honest blocking docs, config key, ADR, property test, revert localized

- A1: rewrote the opt-in-blocking doc + Security changeset honestly — the PostToolUse hook is a
  circuit-breaker (halts the agent's next step), NOT a redactor; it does not scrub content already
  in the transcript. The prompt-level data/instruction boundary is the primary control.
- A2: registered security.injection_blocking in the config schema + defaults manifests (default
  false) + an e2e config-roundtrip test; the dotted setter writes the nested shape the hook reads.
- A3: reverted the 4 hand-edited localized security-model.md (canonical EN only, per convention).
- A5: ADR-1577 (untrusted-input boundary + opt-in blocking; redaction-vs-circuit-breaker rationale).
- A6: property test — scanner never crashes / only emits valid JSON on unicode/large/malformed input.
- Also: inventory (untrusted-input-boundary.md) + agent-size baseline (8 ingest agents) +
  drift-guard matcher update (Read -> Read|WebFetch|WebSearch). A7 (content<20 early-exit) left as
  the noted pre-existing follow-up.

* fix(#1577): allowlist untrusted-input-boundary.md in injection-scan CI gate

The new reference quotes injection phrases ('ignore previous instructions',
'you are now…') as examples agents must NOT comply with, tripping the repo's
own prompt-injection-scan.sh diff gate (the standalone 'security' CI job, red
on HEAD). Allowlist it alongside the other security docs (security-model.md,
TEST-EXAMPLES.md) that legitimately demonstrate injection patterns. The JS
scanner test doesn't scan references/, so only the shell gate needed it.

Verified: scan --diff origin/next -> 0 findings; scanner JS test 15/15.

* fix(#1577): cover AC #2's gsd-ui-researcher + gsd-assumptions-analyzer

trek-e Major 1: the @-included set dropped two AC #2 agents. Restore them so
no named web-ingress agent is uncovered, keeping the two justified additions
(gsd-ai-researcher, gsd-domain-researcher). Final set = AC's 8 + 2 = 10.
 - gsd-ui-researcher carries the full WebSearch/WebFetch + MCP-fetch toolset.
 - gsd-assumptions-analyzer reads 5-15 codebase source files (external/source-
   document ingress per the boundary), though it has no web tools.
INGEST_AGENTS in the isolation test now asserts all 10; size baselines
regenerated (+60 bytes each, both well under the DEFAULT cap); changeset
reworded 8 -> 10.

Verified: untrusted-input-isolation 14/14; agent-size-budget 39/39.

* docs(#1577): document security.injection_blocking + boundary seam

trek-e Major 2 + Minor:
 - docs/CONFIGURATION.md: add the top-level security.injection_blocking key to
   the Full Schema and a Security Settings subsection, distinguishing it from
   the workflow.security_* namespace; honest circuit-breaker-not-redactor
   framing matching ADR-1577 / security-model.
 - CONTEXT.md: add the 'Untrusted-input boundary' seam glossary entry.

Verified: lint:docs ok; config-field-docs + contributor-standards green.

* test(#1577): make read-injection property test git-text, not binary

trek-e nit (and more): the file embedded a raw U+FFFF AND a raw NUL byte as
degenerate-edge inputs. The NUL is what actually made git classify it binary
(git binary = NUL in first 8K). Replace both with text-safe escapes that keep
the identical runtime values: '\\x00' and String.fromCodePoint(0xFFFF). File
now diffs/blames line-by-line.

Verified: property test 2/2; no NUL/raw-noncharacter bytes remain.

* docs(#1577): align untrusted boundary docs

Name all 10 ingress agents in INVENTORY/security-model and allowlist the intentional read-injection property corpus for the prompt-injection scanner.

* docs(#1577): align ADR ingest agent count

Update ADR-1577 from 8 to 10 ingest agents so it matches the actual boundary include set and the rest of the docs.

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-06-24 17:07:23 -04:00
Tom Boucher
07adeb50a0 fix(#1342): scope worktree-path-guard to GSD executor runs; fail open for no-repo targets (#1361)
* fix(#1342): scope worktree-path-guard to GSD executor runs; fail open for no-repo targets

The PreToolUse worktree-path-guard fired for any Write/Edit in any linked git
worktree, with no check for active GSD work — so Claude Code plan-mode writing
~/.claude/plans/<slug>.md from a manually-created worktree was hard-blocked.

- Gate enforcement on the GSD isolated-executor branch namespace
  (^worktree-agent-[A-Za-z0-9._/-]+$, per worktree-branch-check.md #2924); the
  guard is a no-op in non-GSD linked worktrees.
- Fail open when a target resolves to no git repository (e.g. ~/.claude/plans/)
  instead of blocking — that is not the #260 main-repo vector. A target inside
  a .git directory still blocks (git rev-parse --is-inside-git-dir).
- The #260 different-git-root hard block (escape to the main repo) is preserved.

Detached-HEAD executors no-op the gate; this is accepted because they are
fail-closed by worktree-branch-check.md (exit 42) before committing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#1342): add changeset for worktree-path-guard scoping fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#1342): build dot-dot traversal path portably (Windows drive-letter fix)

The traversal test built its file_path by stripping a leading slash from an
absolute externalDir and path.join-ing it after a `..` chain. On Windows the
drive letter (C:\) is not a leading slash, so it survived and path.resolve
produced an invalid doubled-drive path (C:\C:\Users\...), which resolves to no
git repo — the hook failed open (exit 0) and the test expected a block (exit 2).

Use path.relative(worktreeDir, externalTarget) + string concat so the file_path
carries literal `..` segments that resolve to externalTarget on both posix and
win32 (no drive doubling). Verified with path.win32/path.posix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:50:28 -04:00
Tom Boucher
d444864bf8 fix(#1194): correct inverted statusline auto-compact buffer math (#1211)
* 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>
2026-06-14 10:46:00 -04:00
Tom Boucher
9e5d4b266b fix(#997): ensure canonical ~/.claude/gsd-core path for plugin installs (#1207)
* fix(#997): ensure canonical ~/.claude/gsd-core path for plugin installs via SessionStart hook

Claude Code marketplace plugin installs unpack the package into the
version-pinned plugin cache and never run bin/install.js, so
~/.claude/gsd-core/ is never created. Agents, commands, and templates
markdown-@-include the canonical ~/.claude/gsd-core/... path (which
expands ~ but NOT ${CLAUDE_PLUGIN_ROOT}), so every include resolved to
nothing and agents (e.g. the executor) failed.

Add a SessionStart hook (hooks/gsd-ensure-canonical-path.js) that, on a
plugin install, symlinks the canonical path's immutable subdirs (bin,
contexts, references, templates, workflows) to the plugin's bundled
gsd-core/ tree. It changes zero @-references, is a no-op in classic
installs, preserves user-generated files (USER-PROFILE.md, STATE.md),
prunes stale links so it self-heals after `claude plugin update`, uses
Windows junctions, and rejects bundled/canonical paths that escape the
resolved plugin root (no traversal, no clobber).

Registered in HOOKS_TO_COPY (build-hooks), MANAGED_HOOKS, hooks.json
SessionStart (runs first, timeout 5), and BUNDLED_GSD_HOOK_FILES.
Behavioral regression tests folded into issue-766-plugin-manifest.test.cjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(#997): backfill changeset PR number to #1207

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 09:37:41 -04:00
Tom Boucher
cf6d3b3be5 fix(#925): context monitor echoes the invoking hook event name (#927)
Read `data.hook_event_name` from the stdin payload and fall back to the
Gemini/non-Gemini heuristic only when the field is absent or blank.
Fixes Claude Code rejecting output with "expected Stop but got PostToolUse"
when the monitor is called by Stop, SubagentStop, or PreCompact hooks.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-09 08:42:16 -04:00
Tom Boucher
1b6bd66f2c feat(#770): register Claude Code lifecycle hooks (SubagentStop/Stop/PreCompact/FileChanged) (#821)
* feat(#770): register Claude Code lifecycle hooks (SubagentStop/Stop/PreCompact/FileChanged)

Wire three new context-tracking events (SubagentStop, Stop, PreCompact) to
gsd-context-monitor so context-headroom warnings surface at model-stop and
subagent-finalisation moments — not just on PostToolUse.  Add a new
FileChanged hook (gsd-config-reload.js) that hot-reloads .planning/config.json
context mid-session when the user edits it, injecting a config summary as
hookSpecificOutput.additionalContext.  Updates plugin manifest hooks.json,
managed-hooks-registry, installer-migration-report allowlist, and
shell-command-projection cleanup tables.  Tests: 21 new assertions in
enh-770-claude-hook-events.test.cjs; enh-788 and issue-766 test suites updated.

Closes #770

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#770): document newly-registered Claude Code lifecycle hooks

Add a Hook coverage table to the Claude Code npm installer section of
docs/how-to/install-on-your-runtime.md describing SubagentStop, Stop,
PreCompact, and the new FileChanged (gsd-config-reload.js) hook that
hot-reloads .planning/config.json mid-session. Also fixes the changeset
frontmatter (adds type: Added + pr: 821) so docs-lint can consume the
fragment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#770): add gsd-config-reload.js to INVENTORY.md and regenerate manifest

The feat commit added hooks/gsd-config-reload.js but did not bump the
Hooks count in docs/INVENTORY.md (14→15) or add the new row, and did not
regenerate docs/INVENTORY-MANIFEST.json. Both inventory-counts and
inventory-manifest-sync tests failed across the full CI matrix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#770): make lifecycle-hook tests deterministic on scoped runner

Replace the shared hooks/dist/ ensemble setup (ensureHooksDist /
teardownHooksDist) in the Claude hook tests with per-test isolation:
pre-populate each test's own tmpDir/.claude/hooks/ with stub files and
pass installerMigrations:[] to install() so the first-time-baseline
migration does not remove the stubs before the copy step can run.

Root cause: hooks/dist/ is gitignored and absent on a fresh npm ci.
ensureHooksDist() created it and teardownHooksDist() deleted it, but
with --test-concurrency=4 both test files ran concurrently as separate
Node.js worker processes sharing the same filesystem.  One file's
afterEach teardown deleted hooks/dist/ while the other file's install()
was copying from it, producing an ENOENT (reproduced 2/10 runs locally).

The additional issue: even with pre-placed stubs surviving the copy race,
the 000-first-time-baseline migration classified hooks/gsd-*.js as
bundled-gsd-hook artifacts, auto-removed them, and the copy step never
re-ran (hooks/dist/ absent) — leaving contextMonitorFile missing and all
hook registrations silently skipped (the 'got: []' symptom).

Fix: pre-populate targetDir/hooks/ per-test (isolated temp dir) AND pass
installerMigrations:[] so the baseline scan is skipped.  The Qwen suites
already used this pattern correctly; the Claude suites are aligned to it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#770): ship gsd-config-reload.js by adding it to build-hooks HOOKS_TO_COPY

The #770 feature added hooks/gsd-config-reload.js and registered it in
MANAGED_HOOKS, the installer, INVENTORY, and the test EXPECTED_ALL_HOOKS
list — but never added it to scripts/build-hooks.js HOOKS_TO_COPY. As a
result the hook was never copied into hooks/dist/ during the build, so:

  - the hook would never ship to users (real production bug — the
    FileChanged config-reload feature was dead-on-arrival), and
  - install-minimal-hooks.test.cjs #1755 ("all expected hooks are copied
    from hooks/dist/ to target", ".js hooks are executable after copy",
    "manifest contains .js hook entries") failed on any environment with
    a clean checkout (no pre-existing hooks/dist/): coverage, full test
    macos-22/macos-24, test ubuntu-24.

The failures were masked locally only by a stale hooks/dist/ left from a
prior build (build-hooks copies into dist without clearing it). On CI's
fresh `npm ci` there is no dist, so the omission surfaced.

Fix: add 'gsd-config-reload.js' to HOOKS_TO_COPY so build-hooks stages it
into hooks/dist/ alongside the other JS hooks. Verified by removing
hooks/dist/ and rerunning the full suite green (0 fail).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#770): make config prototype-pollution beforeEach deterministic on scoped runner

Root cause: the #663 and alert-#26 prototype-pollution describe blocks
seeded .planning/config.json in beforeEach via a bare
runGsdTools('config-ensure-section') whose result was discarded. That
command runs in a spawned gsd-tools child; on the scoped CI lane
(--test-concurrency=4, config.test.cjs scheduled alongside the heavy
install/tarball suites that #770 pulled into the targeted set) the child
can be transiently killed under resource pressure (non-zero exit, empty
stderr — an OS-level kill, not an app error). The swallowed failure left
config.json absent, so the first subtest's readConfig() threw ENOENT
opening <tmp>/.planning/config.json. Only 1 of 4 subtests failed,
confirming a per-invocation transient, not a deterministic miss; the full
suite schedules files differently so config.test.cjs did not collide with
those heavy neighbors → passed there.

Fix: add ensureConfigReady(tmpDir) which retries config-ensure-section on
ANY failure or missing file and throws a clear diagnostic if it still
cannot create config.json, then use it in both prototype-pollution
beforeEach blocks. Setup is now deterministic under load; the #663/alert-#26
security assertions are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 20:36:11 -04:00
Tom Boucher
1040fb792e feat(#777): register Cursor-native hooks (.cursor/hooks.json) for session-start/post-tool parity (#831)
* feat(#777): register Cursor-native hooks (.cursor/hooks.json) for session-start/post-tool parity

- Add gsd-cursor-session-start.js: injects STATE.md presence reminder (or
  new-project nudge) into Cursor sessions via the sessionStart hook event
- Add gsd-cursor-post-tool.js: emits an additional_context nudge when
  write-class tool calls touch .planning/ files (postToolUse hook event)
- Add 'cursor-hooks-json' installSurface to runtime-config-adapter-registry;
  writeCursorHooksJson/reconcileCursorHooksJson write the canonical
  { version: 1, hooks: { sessionStart, postToolUse } } JSON shape with
  idempotent reconciliation that preserves user-owned hook entries
- Hook scripts are copied with /gsd:→gsd- rewrite so installed files
  contain no colon-form slash-command refs (bug-376 invariant)
- 20 new tests in tests/cursor-hooks.test.cjs cover all reconciler paths,
  entry helpers, removal, runtime adapter surface, and hook script behavior
- Update CONTEXT.md, ARCHITECTURE.md, installer-migrations.md, and
  000-first-time-baseline.cts to include Cursor hooks.json surface

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#777): build hooks/dist on demand in bug-376 test for scoped/windows CI

hooks/dist is gitignored and only produced by `npm run build:hooks`.
The CI scoped (ubuntu-latest/node-22) and windows (windows-latest/node-24)
test jobs do NOT run build:hooks before executing tests, so bug-376's
prerequisite suite was failing with "hooks/dist not found" on both legs.

Add ensureHooksDist() helper (mirrors bug-3357 pattern) that builds
hooks/dist on demand in the before() hooks of prerequisite and Suite 3.
Also add ensureHooksDist() call to Suite 3's before() so the snapshot
step is also hermetic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 19:22:48 -04:00
Tom Boucher
f66c4a082c feat(#766): distribute gsd-core as a native Claude Code plugin (#797)
* feat(#766): distribute gsd-core as a native Claude Code plugin

Add an additive .claude-plugin/plugin.json manifest plus hooks/hooks.json so gsd-core can be installed as a first-class Claude Code plugin (marketplace or zero-friction @skills-dir), with /gsd-core: namespaced commands and lifecycle management — alongside the unchanged npm/file-copy installer.

- .claude-plugin/plugin.json: validated with 'claude plugin validate --strict'
- hooks/hooks.json: mirrors the installer's always-on Claude hook wiring via ${CLAUDE_PLUGIN_ROOT}
- package.json: ship .claude-plugin in the npm tarball
- tests/issue-766-plugin-manifest.test.cjs: manifest + always-on-hook-contract drift guards
- docs: install-on-your-runtime.md + FEATURES.md

Closes #766

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#766): add ADR-766 + glossary entry for Claude Code Plugin Manifest Module

Record the plugin manifest as the Seam projecting gsd-core's artifact surfaces onto the Claude Code plugin contract (sibling of the Runtime Artifact Layout Module, ADR-3660), with the defined kind->field mapping and the always-on hook projection rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:02:39 -04:00
Tom Boucher
10f6981743 fix(#685): set windowsHide on all Windows child-process spawns (#688)
* fix(#685): set windowsHide on all Windows child-process spawns

A visible "gsd-core" console window flashed on Windows whenever a gsd-core
child process spawned without `windowsHide: true`. The most visible offenders
fire on every SessionStart / `/clear` (execNpm's `shell:true` npm view via the
update-check worker) and on every Edit/Write/MultiEdit in a worktree (the
worktree-path guard's git probe).

Add `windowsHide: true` to every external-binary spawn in the runtime source:
- hooks/gsd-context-monitor.js (record-session spawn)
- hooks/gsd-worktree-path-guard.js (SPAWNOPT)
- hooks/gsd-workflow-guard.js (git branch --show-current)
- src/shell-command-projection.cts (execGit / execNpm / execTool)
- src/check-command-router.cts (git log execFileSync)
- src/roadmap-upgrade.cts (git status/rev-parse/reset/clean execSync)

gsd-check-update.js already had it (the precedent). probeTty's tty call is
POSIX-only and intentionally untouched. Adds a regression test that asserts
each site plus a repo-wide completeness guard so a future external-binary
spawn that omits windowsHide fails CI. No behavior change off-Windows.

Closes #685

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#685): set changeset pr number to 688

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 10:35:22 -04:00
Tom Boucher
463cffd894 chore(#604): rename get-shit-done/ runtime directory to gsd-core/ (#615)
* 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>
2026-06-02 18:35:29 -04:00
Tom Boucher
91f5bd7cb1 feat(#607): rebuild get-shit-done-cc → gsd-core migration (per-package cache + installer auto-cleanup + --dry-run) (#611)
* 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>
2026-06-02 12:19:57 -04:00
Tom Boucher
faf329ecb9 fix(#260): enforce worktree absolute-path safety via PreToolUse hook
Closes #260

Moves the step-0b absolute-path guard from prose instructions to a harness-enforced PreToolUse hook (gsd-worktree-path-guard.js). Hard-blocks Edit/Write/MultiEdit calls whose absolute path resolves outside the active worktree root.
2026-06-01 10:56:06 -04:00
Tom Boucher
0fbe1d899e chore(#191): retire the gsd-sdk shim — route everything at gsd-tools (#522)
* chore(#191): migrate gsd-sdk query call sites to gsd-tools query

Retiring the gsd-sdk shim. gsd-tools.cjs already accepts `query` as a
meta-prefix (gsd-tools query <command>), so this is a behavior-preserving 1:1
swap across the runtime reference prompts, the graphify hook's commit-detection
gate, and two bin/lib comment/message references.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#191): remove vestigial gsd-sdk shim code from installer + projection

The gsd-sdk shim was already not wired up (no gsd-sdk bin in package.json;
buildWindowsShimTriple had zero call sites). Remove the dead code:
- shell-command-projection.cjs: buildWindowsShimTriple + formatSdkPathDiagnostic
  (+ their now-unused PACKAGE_NAME import) and exports
- install.js: the re-export wrappers + imports, the #3406 stale-standalone-sdk
  detection (detectStaleStandaloneSdk/formatStaleStandaloneSdkWarning + its
  global-install call site), and the exports

Preserved (retained, not gsd-sdk): buildCodexHookWindowsShimIR (#3426) — only
its comments referenced the gsd-sdk pattern; reworded. Also kept the
homePathCoveredByRc 'reopen your shell' branch in maybeSuggestPathExport — its
logic is bin-dir-agnostic, only the message mentioned gsd-sdk; reworded to use
the actual bin dir.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#191): update tests for retired gsd-sdk shim

- bug-3441/bug-3442: drop the formatSdkPathDiagnostic / buildWindowsShimTriple
  assertions (functions removed); retained PATH-action + drift-guard tests stay
- bug-505: remove the 'still exported' assertions for detectStaleStandaloneSdk /
  formatStaleStandaloneSdkWarning / the shim contract surface (#505 kept them;
  #191 removes them)
- graphify-auto-update: migrate the hook-dispatch inputs gsd-sdk query commit ->
  gsd-tools query commit to match the migrated commit hook

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#191): point active docs at gsd-tools query (gsd-sdk shim retired)

Update the user/agent-facing docs (AGENTS, COMMANDS, CONFIGURATION, USER-GUIDE,
ship-pr-body-sections) that presented gsd-sdk query as a current command to
gsd-tools query. Historical docs (ADRs, PRDs, release notes) left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#191): correct state.load vs state.json description for gsd-tools query

Adversarial-review (codex) finding: the migrated USER-GUIDE line claimed both
'gsd-tools query state.json' and 'state.load' resolve to the frontmatter-rebuild
handler. Verified they don't — state.load returns the CJS load shape
(config + state_raw + flags), state.json returns the frontmatter shape. Both are
available via gsd-tools query; corrected the text to say so.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#191): add changeset for gsd-sdk shim retirement

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 19:19:14 -04:00
Tom Boucher
6f2520786d feat(#498): single Package Identity seam for /gsd:update + fix runtime undefined-name bug (#499)
* feat(#498): generated package-identity seam derived from package.json

Introduce a single source for GSD's published-package coordinates:
scripts/generate-package-identity.cjs (pure deriveIdentity + formatManualInstall
+ render) emits the generated get-shit-done/bin/lib/package-identity.cjs with
values baked from package.json at build time. Baking is required because the
installed tree carries only a synthetic {"type":"commonjs"} package.json, so a
runtime require('package.json').name resolves to undefined (#378). Reconciles

Wired into npm run build; a parity test fails CI if the committed file drifts
from package.json.

Refs #498

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#498): repoint update worker + check-latest-version at the seam

- check-latest-version.cjs sources PACKAGE_NAME from the package-identity seam
  instead of a re-typed literal (single source; #2992's constant guarantee is
  preserved since the seam bakes from package.json).
- gsd-check-update-worker.js no longer does require('../package.json').name
  (resolved to undefined in the installed tree → background update check
  silently broken, #378). It now delegates the latest-version lookup to
  checkLatestVersion(), collapsing the duplicated npm-view call onto the single
  deterministic adapter and inheriting its typed {ok,version,reason} surface.
- Move the PR #3102 Windows shell-gate contract test onto execNpm (where the
  spawn now lives) and assert the worker no longer spawns npm directly.
- Rewrite the #378 contract: worker must NOT use require(package.json).name and
  must delegate; check-latest-version PACKAGE_NAME is single-sourced from the seam.

Fixes #378-class runtime breakage. Refs #498

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#498): changeset for package-identity seam + update-check fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#498): drift-guard lint — value-check GSD coordinate literals against the seam

scripts/lint-package-identity-drift.cjs scans the runtime/code surface
(bin/, hooks/, scripts/, get-shit-done/) and asserts every GSD package name
and GitHub repo slug literal equals the Package Identity seam's current value.
Passes today; fails the moment a repoint isn't propagated (rename package.json,
regenerate the seam, and stale literals are reported until updated). This is
the second adapter that makes the seam real and a repoint mechanically safe.

Enforced via tests/issue-498-identity-drift-lint.test.cjs (scanRepo === [])
under npm test; also exposed as `npm run check:identity-drift`.

Refs #498

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#498): update-context projection — port update.md resolution to a tested seam

Add get-shit-done/bin/lib/update-context.cjs: a pure, injected-fs port of
update.md's ~280-line get_installed_version bash. resolveUpdateContext()
reproduces the full precedence cascade (preferred fast-path -> local probe ->
global probe via env overrides then $HOME -> LOCAL-if-distinct -> scope
cascade -> UNKNOWN) and returns the 4-field contract { installedVersion,
scope, runtime, gsdDir }. The fs is injected so every branch is finally
testable without a live multi-runtime install.

Expose it as `gsd-tools update-context [--config-dir <d>] [--runtime <r>] --json`.
Purely additive — update.md is unchanged in this commit; the workflow swap
follows separately.

Refs #498

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(#498): swap update.md resolution to the update-context projection

Replace ~280 lines of inline runtime/scope/config-dir bash in update.md's
get_installed_version step with a call to `gsd-tools update-context --json`
(60 lines: derive PREFERRED_* from execution_context, resolve gsd-tools.cjs,
parse the 4-field JSON). Behavior is unchanged — the projection reproduces the
same cascade — but the logic is now tested in update-context.cjs instead of
untestable bash-in-markdown.

Relocate the #3608 antigravity-first-class contract onto the projection
(RUNTIME_DIRS order, inferPreferredRuntime, envRuntimeDirs) plus a behavioral
test; keep the execution_context path-classification assertion on update.md.
Re-point install.test's custom-config-dir assertion (kilo.jsonc/KILO_CONFIG)
to update-context.cjs where that detection now lives.

Full root suite: 2022 pass / 0 fail.

Refs #498

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(#498): record Update Context Module in CONTEXT.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#498): CI — avoid bare gsd-tools in update.md; register new CLI modules

- update.md update-context invocation: resolve the PATH gsd-tools shim into a
  variable and call "$GSD_TOOLS" (never a bare `gsd-tools` command) — satisfies
  the #2851 workflow-bare-gsd-tools guard.
- Register package-identity.cjs and update-context.cjs in docs/INVENTORY.md
  (CLI Modules 76 -> 78 + rows) and regenerate docs/INVENTORY-MANIFEST.json,
  fixing inventory-counts and inventory-manifest-sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#498): make update-context + parity tests OS-agnostic (Windows CI)

Two Windows-only test failures, both test-portability (production code is fine —
the real-fs CLI integration test passed on Windows):

- update-context resolver tests + bug-3608 behavioral test used POSIX path-string
  keys in their fake fs, but the resolver builds lookups via path.join/resolve
  (backslash + drive letter on Windows) → keys never matched → everything
  resolved to UNKNOWN/claude. Normalize fake-fs keys and gsdDir comparisons
  through path.resolve so they match on both platforms.
- package-identity parity test compared render() (LF) to the committed file,
  which Windows git checks out as CRLF (no .gitattributes eol rule). Normalize
  line endings before comparing, matching the repo convention
  (autonomous-decomposition, bug-3707).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#498): update.md backup must use GSD_DIR (adversarial-review finding)

The get_installed_version rewrite emits GSD_DIR but dropped the probe-loop
variables LOCAL_DIR/GLOBAL_DIR. The backup_custom_files step still read those,
so RUNTIME_DIR went empty for every LOCAL/GLOBAL install and detect-custom-files
was skipped — and since the update then runs a clean install that wipes managed
dirs (commands/gsd, get-shit-done), user-added files could be deleted without
the intended backup.

Set RUNTIME_DIR="$GSD_DIR" directly (the resolved config dir; empty for
UNKNOWN scope, which still skips the backup). Add a structural regression
(tests/issue-498-update-backup-runtime-dir.test.cjs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#503): re-point Antigravity .agent detection at the #498 projection

#499 moves the runtime/scope detection cascade out of update.md inline bash
into get-shit-done/bin/lib/update-context.cjs. The #503 regression test asserted
on the inline RUNTIME_DIRS array, which no longer exists, so it would fail
against the projected update.md even though the .agent guarantee is preserved.

Rewrite it to verify the surviving surfaces:
 - behavioral: resolveUpdateContext resolves a LOCAL ./.agent install to the
   antigravity runtime (the original root cause, now covered by adding
   ['antigravity', '.agent'] to the projection RUNTIME_DIRS table)
 - update.md prose classifier still maps /.agent/ -> antigravity
 - the post-update cache-clear for-dir loop still includes .agent

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#498): finish de-hardcoding consumers + close adversarial-review parity gaps

Restore the consumer de-hardcoding that is the point of the seam, and close the
parity gaps an adversarial review (codex) found in the update-context projection.

De-hardcode the repo slug + install command in the changeset tooling — #516
only single-sourced the package NAME, leaving 'open-gsd/get-shit-done-redux'
hardcoded in scripts/changeset/cli.cjs and github-release-notes.cjs. Route both
through the seam's repoSlug/packageName so a rename is a regenerate, not a hand
edit. The drift-lint real scan now reports zero divergent coordinate literals.

Projection parity vs the old inline bash, as ONE consistent rule
(trustedVersionAt) applied on every path:
 - expand a leading ~/ in preferredConfigDir before the fast path (the bash ran
   expand_home first; a custom --config-dir ~/foo otherwise fell to UNKNOWN)
 - trust a version only when BOTH VERSION and the update.md marker exist — fast
   path AND LOCAL/GLOBAL cascade; a partial dir falls to 0.0.0 keeping scope
 - apply the same same-path dedup to the 0.0.0 fallback so a partial install
   probed from cwd===home is not misdetected as LOCAL

Adds regression tests for tilde expansion, VERSION-only (cascade + fast path),
and the cwd===home partial-install dedup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 16:53:44 -04:00
Tom Boucher
b8c33647d8 refactor(tests): retire output-grep & source-grep via typed surfaces (finish #2974) (#462)
* refactor(#455): implement typed surfaces to retire grep tests

Production surfaces added:
- hooks/managed-hooks-registry.cjs: new CJS module exporting MANAGED_HOOKS
  as a typed array; gsd-check-update-worker.js now requires it instead of
  declaring an inline array
- bin/install.js: elevate inline gsdHooks to module-level GSD_UNINSTALL_HOOKS,
  export it alongside runtimeMap/allRuntimes (already exported)
- scripts/build-hooks.js: export HOOKS_TO_COPY; guard build() behind
  require.main===module so tests can require the file without triggering a build
- get-shit-done/bin/lib/init.cjs: add --json mode to agent-skills command,
  emitting typed IR { agent_type, block, skills_count } for test assertions
- get-shit-done/bin/gsd-tools.cjs: wire --json flag for agent-skills dispatch

Category-B source-grep migrations:
- tests/managed-hooks.test.cjs: require MANAGED_HOOKS from registry, drop fs.readFileSync+regex
- tests/orphaned-hooks.test.cjs: require MANAGED_HOOKS+HOOKS_TO_COPY as typed exports
- tests/hooks-opt-in.test.cjs: replace gsdHooks regex-parse with GSD_UNINSTALL_HOOKS import
- tests/install-minimal-hooks.test.cjs: replace gsdHooks regex-parse with GSD_UNINSTALL_HOOKS
- tests/copilot-install.test.cjs: replace src.includes() checks with typed
  assertions on runtimeMap, allRuntimes, parseRuntimeInput, buildRuntimePromptText
- tests/agent-skills.test.cjs: migrate to --json typed IR assertions

pending-migration-to-typed-ir token cleared (87 of 87 files):
- 78 files already had source-text-is-the-product; removed duplicate token
- 5 files already used typed assertions; reclassified or annotated
- 4 files required individual reclassification to source-text-is-the-product
  or architectural-invariant

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#455): update workflow-guard test to typed GSD_UNINSTALL_HOOKS import; isolate HOME in runtime-launcher (D) test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#455): guard install.js main() behind require.main===module so the typed export is require-safe

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(#455): document --json typed surfaces for agent-skills, progress, validate context

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(#455): add changeset fragment for new --json surfaces

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(#455): complete grep migration for files flagged by lint-tests

The branch commit 4e630d99 stripped `allow-test-rule: pending-migration-to-typed-ir`
from ~80 test files without replacing their assertions or adding the correct
exemption annotation. The files were NOT source-grep tests — they read .md
workflow/agent/command/reference files (source-text-is-the-product) or hook
source files for structural invariants (structural-regression-guard). No
assertion logic was changed; only the correct allow-test-rule annotation was
added to each file per CONTRIBUTING.md exception matrix.

73 files: `source-text-is-the-product` — workflow/agent/command/reference .md
7 files:  `structural-regression-guard` — hook .js / bin/install.js structural checks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: CI Rebase Check <ci@gsd-redux>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 11:39:52 -04:00
Tom Boucher
7ea6a06645 fix(#378): poll scoped package name in update check (#414)
The update worker queried the unscoped 'get-shit-done-redux' via
`npm view`, which returns E404 — so `latest` stayed null and
`update_available` could never become true. Now derives the name from
package.json (`require('../package.json').name`) so it always matches
the actual published scoped name (@opengsd/get-shit-done-redux).

Fixes #378.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:10:07 -04:00
Tom Boucher
ee820442e6 perf(#317): collapse redundant existsSync+readFileSync in context-monitor hook (#400)
Per-PostToolUse hot path did stat-then-read ×3 (config.json, metrics bridge,
warn sentinel); collapsed to read-with-ENOENT-catch (fewer blocking syscalls,
no TOCTOU), behavior identical. Fixes #317.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:21:55 -04:00
Tom Boucher
091b7e2c4b perf(#305): single-pass max-by-mtime for statusline todo lookup (#385)
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>
2026-05-27 20:21:15 -04:00
Colin
3002bd73e0 fix: respect git add option terminator 2026-05-25 10:19:54 -04:00
Colin
643efb1d76 fix: honor workflow guard opt-in for Bash blocks 2026-05-25 10:07:40 -04:00
Colin
69cc2f9fe2 fix(issue): bug: worktree executor force-adds gitignored .planning/SUMMARY.md into worktree branch (regression of the skipped_gitignored SDK contract) 2026-05-25 09:58:58 -04:00
Tom Boucher
6913dbcdb1 fix(10): centralize semver comparison policy across hooks and changeset 2026-05-24 18:14:56 -04:00
Tom Boucher
e32a53b974 feat(113): detect javascript:/data:/userinfo/token-in-query in markdown links (#133)
* test(113): add per-rule failing tests + hostile fixture for markdown link payloads

RED phase for issue #113 — scanForInjection() currently returns { clean: true }
for markdown links containing javascript:, data:text/html, userinfo credentials,
and token-in-query payloads.

Changes:
- tests/fixtures/adversarial/security/context-malicious-markdown-link.md:
  Extended to contain one hostile example per rule class (MD-LINK-JS-SCHEME,
  MD-LINK-DATA-SCHEME, MD-LINK-USERINFO, MD-LINK-TOKEN-IN-QUERY) plus benign
  negative controls (data:image/png, mailto:, https://github.com, port-only URL).
- tests/security-prompt-injection.test.cjs:
  - Flipped PINNED "malicious-markdown-link fixture is NOT flagged" assertion
    to "malicious-markdown-link fixture is flagged by scanner" (forward-looking).
  - Added 4×positive + 4×negative per-rule unit tests asserting structuredFindings
    with ruleId, file, line, match fields.
  - Added parity guard: every MARKDOWN_LINK_PATTERNS source string from
    security.cjs must appear in gsd-read-injection-scanner.js hook source.

D3 false-positive grep: 0 legitimate matches — no allowlist entries needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(113): detect javascript:/data:/userinfo/token-in-query in markdown links (security.cjs + hook)

GREEN phase for issue #113.

Rule details (all with primary source citations):

  MD-LINK-JS-SCHEME
    Flags ](javascript:...) regardless of case.
    Source: OWASP XSS Prevention Cheat Sheet
    https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html

  MD-LINK-DATA-SCHEME
    Flags data: URIs NOT in the explicit safe-list.
    Safe-list: image/(png|jpeg|gif|webp|bmp|ico|avif|heic) and font/(woff2?|otf|ttf).
    data:image/svg+xml is intentionally BLOCKED — SVG can host <script>.
    Source: OWASP File Upload Cheat Sheet — SVG Files
    https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html#svg-files

  MD-LINK-USERINFO
    Flags https?://user:pass@host in markdown link targets.
    Does NOT fire on: mailto:user@host (no :// before user) or https://host:443/path (port, not userinfo).
    Source: RFC 3986 §3.2.1 (userinfo syntax)
    https://www.rfc-editor.org/rfc/rfc3986#section-3.2.1
    RFC 9110 §4.2.4 (HTTP deprecates userinfo)
    https://www.rfc-editor.org/rfc/rfc9110#section-4.2.4

  MD-LINK-TOKEN-IN-QUERY
    Flags key NAMES: token, access_token, id_token, refresh_token, api_key, apikey,
    secret, password, client_secret, code — regardless of value.
    Source: RFC 9700 OAuth 2.0 Security BCP §4.3.1
    https://www.rfc-editor.org/rfc/rfc9700#section-4.3.1
    D3 false-positive grep: 0 legitimate matches in codebase — no allowlist needed.

Architecture:
- scripts/security.cjs: canonical MARKDOWN_LINK_PATTERNS export, scanForInjection()
  extended with structuredFindings (ruleId, file, line, match) via opts.file.
- hooks/gsd-read-injection-scanner.js: patterns inlined for hook independence
  (same pattern sources, verified by parity test).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(113): flip PINNED malicious-markdown-link assertion and add parity guard

REFACTOR phase — tightening test rigor after test-rigor skill review:

1. Fixture assertion now enumerates all 4 expected ruleIds explicitly:
   [MD-LINK-JS-SCHEME, MD-LINK-DATA-SCHEME, MD-LINK-USERINFO, MD-LINK-TOKEN-IN-QUERY].
   Previously findings.length > 0 would pass even if 3 of 4 rules were broken.

2. line field assertions tightened: `f.line >= 1` (meaningful lower bound for
   1-based line numbers) instead of `typeof f.line === 'number'` (vacuous).

3. match field assertions tightened to check the hostile content is present:
   - MD-LINK-JS-SCHEME: /javascript:/i in match
   - MD-LINK-DATA-SCHEME: /data:/i in match
   - MD-LINK-USERINFO: /@/ in match (the @ character is the definitive userinfo marker)
   - MD-LINK-TOKEN-IN-QUERY: /token=/i in match

4. Parity test checks actual RegExp .source strings (not just lengths), verifying
   the hook contains the exact canonical pattern sources character-for-character.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(#113): add changeset fragment + Windows/Node 24 state.test compatibility

1. .changeset/113-malicious-markdown-links.md — required Security fragment
   for the user-facing markdown-link scanner changes in this PR (changeset-lint
   was failing with FAIL_MISSING_FRAGMENT).

2. get-shit-done/bin/lib/state-command-router.cjs — add OUTPUT_ON_SDK_ERROR
   set for mutation state subcommands whose CJS contract is always exit-0.
   On Windows/Node 24 the SDK bridge returns result.ok===false for validation
   failures (e.g. state record-metric --phase 1 with no --plan/--duration),
   causing dispatchViaSdk() to call error() (exit 1) instead of output({error})
   (exit 0). The fix maps SDK non-ok results to JSON output for the affected
   mutation commands (record-metric, advance-plan, record-session, add-decision,
   add-blocker, resolve-blocker, update-progress), restoring the exit-0 CJS
   contract on all platforms.

tests/state.test.cjs:1161 "returns error when required fields missing" passes
locally (104/104 pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 10:15:03 -04:00
Tom Boucher
dff176bfd2 chore: rebrand to GSD-redux/get-shit-done-redux
Mirror of code, issues, and PRs from the upstream gsd-build/get-shit-done,
which appears compromised or abandoned (maintainer unreachable since
2026-04-01; $GSD token linked to rug-pull).

- Adds rebrand notice block at top of English README
- Removes $GSD token badge and @gsd_foundation X badge (keeps Discord)
- Renames npm packages: get-shit-done-cc -> get-shit-done-redux,
  @gsd-build/sdk -> @gsd-redux/sdk
- Updates all repo URLs across docs, workflows, package.json, bin/
- Updates ci@gsd-build -> ci@gsd-redux in workflow git identities
- Leaves CHANGELOG and .changeset/* alone (historical, time-stamped)
2026-05-22 08:27:07 -04:00
Cristian Uibar
463eb26544 Match gsd-sdk query commit in graphify auto-update hook (#3653) (#3658)
* Match `gsd-sdk query commit` in graphify auto-update hook (#3653)

The PostToolUse Bash hook only substring-matched direct shell git ops in
tool_input.command. `gsd-sdk query commit` invokes git via spawnSync,
so the literal "git commit" never appears in the Bash tool's command
string and the hook silently skipped every SDK-issued commit. Result:
.planning/graphs/ drifted stale after every phase that closed via
gsd-sdk query commit, with no error and no log.

Gate 2 now also matches `gsd-sdk query commit`. Other SDK verbs
(phase.complete, roadmap.update-plan-progress, state.begin-phase) do
not invoke git themselves and remain non-matching to avoid spurious
rebuilds per state mutation. Adds positive + negative matcher tests.

* Fix changeset frontmatter for #3658

`type: Bug Fix` rejected by scripts/changeset/parse.cjs ALLOWED_TYPES
(Keep a Changelog values: Added/Changed/Deprecated/Removed/Fixed/Security).
Switch to `type: Fixed` and add `pr: 3658` required by MISSING_PR check.

docs-lint now reports `ok_no_triggering_fragments` locally.

* fix(#3658): bound graphify SDK commit matcher

* fix(#3658): exempt release note docs lint
2026-05-18 13:15:15 -04:00
Tom Boucher
f8eda5bf16 fix(3597)(3347): write graphify rebuild lock in parent hook to close ENOTEMPTY race
The hook double-forked the rebuild subprocess and returned before the
subprocess wrote .planning/graphs/.rebuild.lock. Callers (notably the
feat-3347 test cleanup) waited for the lock to disappear before
rm -rf'ing the tmpdir, but an absent lock was ambiguous: it could mean
"subprocess finished and trapped lock removal" OR "subprocess hasn't
started yet." Under ubuntu CI load the second case won, cleanup raced
ahead, and rmSync walked into .planning/graphs while the subprocess
was still creating files — surfacing as
  ENOTEMPTY: directory not empty, rmdir '/tmp/gsd-3347-*/.planning/graphs'
on the "dispatches on: git commit -m fix" test.

Spawn the rebuild as a regular backgrounded job, capture $!, and write
the lock file synchronously in the parent before exit. Lock-presence
is now a reliable in-flight signal; the rebuild script's existing
trap-on-EXIT rm still owns cleanup.

Validated: holodeck (ubuntu docker) 11224 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:39:08 -04:00
Tom Boucher
088fc204ee fix(3347): clear CI gates raised by initial commit
- docs/CONFIGURATION.md: document graphify.auto_update key (config-schema-docs-parity)
- hooks/gsd-check-update-worker.js: add gsd-graphify-update.sh to MANAGED_HOOKS (managed-hooks)
- agents/gsd-planner.md + gsd-phase-researcher.md: slim auto-update awareness block to
  a one-line @-reference; extract full instructions to a new reference file
- get-shit-done/references/planner-graphify-auto-update.md: new reference with the
  status-file schema, the four annotation cases (running/failed/ok-current/ok-stale),
  and interaction with the existing stale-mtime annotation
- docs/INVENTORY.md: References (60 → 61 shipped) + row for new reference; regenerate
  docs/INVENTORY-MANIFEST.json via gen-inventory-manifest.cjs

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:59:43 -04:00
Tom Boucher
dacc23137a feat(3347): opt-in auto-update of knowledge graph after main HEAD advances
Closes #3347

Config:
- Add graphify.auto_update (default false) to manifests:
  sdk/shared/config-defaults.manifest.json, config-schema.manifest.json

Hook:
- hooks/gsd-graphify-update.sh — PostToolUse Bash matcher
  - Gates: tool_name=Bash, HEAD-advancing git op, CI=unset, in git repo,
    current branch == default branch (git.base_branch override or main/
    master/trunk fallback), graphify.enabled && graphify.auto_update both
    true, graphify on PATH, no live PID lock
  - Writes .planning/graphs/.last-build-status.json with status=running
    synchronously, then detaches hooks/lib/gsd-graphify-rebuild.sh
- hooks/lib/gsd-graphify-rebuild.sh — detached rebuild runner
  - PID-lock acquire + trap-on-exit cleanup
  - graphify update . then cp graphify-out/* → .planning/graphs/
  - Status file rewritten to status=ok|failed with exit_code, duration_ms,
    head_at_build
- Portable detach (subshell + disown, no setsid dependency)

Installer:
- bin/install.js: register hook as PostToolUse Bash matcher (5s timeout)
- Add to gsdHooks uninstall list and expectedShHooks warning list

Planner / researcher status surface (issue #3347 reviewer must-have AC):
- agents/gsd-planner.md and agents/gsd-phase-researcher.md
  load_graph_context steps now read .last-build-status.json and surface:
  running → "rebuild in flight"; failed → "auto-rebuild FAILED at {ts},
  context is from prior build"; ok with stale head_at_build → "HEAD has
  advanced since last build"

Settings:
- get-shit-done/workflows/settings.md adds "Graph auto-update" question
  with No-Recommended default; bullets and update_config block updated

Inventory:
- docs/INVENTORY.md hook count 12 → 13 with new row
- docs/INVENTORY-MANIFEST.json regenerated

Tests:
- tests/feat-3347-graphify-auto-update-config.test.cjs (8 tests):
  isValidConfigKey accepts graphify.auto_update, CANONICAL_CONFIG_DEFAULTS
  default false, config-set round-trip, sibling key preservation
- tests/feat-3347-graphify-auto-update-hook.test.cjs (18 tests):
  all bail paths (non-Bash, non-HEAD-advancing, enabled=false,
  auto_update=false, CI=true, non-default-branch, missing graphify bin,
  live-PID lock), dispatch path with mock graphify bin (sync running
  status + detached transition to ok/failed), stale-PID lock, all five
  HEAD-advancing command matchers, git.base_branch override

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:59:43 -04:00
Tom Boucher
e0adba7e08 feat(statusline): add opt-in context_position config for narrow terminals (#2937)
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>
2026-05-14 15:45:41 -04:00
Tom Boucher
a60e05c714 fix(claude): restore namespaced /gsd:<command> references (#3452)
* fix(claude): restore namespaced /gsd:<command> references

* test(claude): align slash-command expectations to /gsd: form

* test(claude): align generated command references to /gsd:

* test(claude): finish /gsd: namespace expectation updates
2026-05-12 21:53:24 -04:00
Tom Boucher
ea37252f20 Merge pull request #3102 from fabiossj83/fix/windows-npm-execfilesync-shell-true
fix(hooks): gsd-check-update-worker — execFileSync 'npm' needs shell:true on Windows
2026-05-06 20:55:48 -04:00
Otavio Salvador
8ca86b5e24 fix: use #!/usr/bin/env bash in community .sh hooks for distro portability
The three opt-in bash hooks (gsd-phase-boundary.sh, gsd-session-state.sh,
gsd-validate-commit.sh) shipped with #!/bin/bash, which fails on distros
that don't ship bash at /bin/bash (NixOS, minimal Alpine images, some
container runtimes). POSIX guarantees /bin/sh but not /bin/bash.

This is latent in the default install path because Claude Code wires the
hooks as `bash <path>` from settings.json (PATH-resolved — the script's
own shebang is read as a comment by bash). The fix matters when scripts
are run directly: tests, future installer changes, or manual debugging.

Changes:
- hooks/gsd-{phase-boundary,session-state,validate-commit}.sh: shebang
  switched to #!/usr/bin/env bash, matching the convention already used
  in scripts/*.sh.
- tests/bug-2136-sh-hook-version.test.cjs: assertion updated to expect
  the new shebang; comment updated to spell out the rationale.
- tests/bug-2979-hook-absolute-node.test.cjs: doc-comment updated — the
  prior wording cited "POSIX std PATH always has /bin" as the reason
  bare `bash` is OK. The actual reason is that bare `bash` is
  PATH-resolved, which is portable across distros that don't ship
  /bin/bash. POSIX std PATH guarantees /bin/sh, not /bin/bash.
- bin/install.js::buildHookCommand: comment block clarifying the same.
  No behavior change in this file — bare `bash` was already correct.
- .changeset/portable-bash-shebang-hooks.md: changeset entry.

Verified locally on NixOS:
- npm run build:hooks: hooks/dist/*.sh shebangs propagate correctly.
- node --test tests/bug-2136-*.cjs tests/bug-2979-*.cjs
  tests/bug-1817-*.cjs tests/bug-1834-*.cjs tests/bug-1906-*.cjs
  tests/bug-2557-*.cjs tests/bug-3017-*.cjs tests/security-scan.test.cjs
  tests/hooks-doc-parity.test.cjs: 126/126 pass.
- node scripts/run-tests.cjs (full suite): 6944 pass / 0 fail / 5 skip.
2026-05-06 15:41:27 -04:00
Tom Boucher
dbc09d21a6 fix(3153): handle numeric 100 percent and block-list next_phases 2026-05-05 18:34:42 -04:00