* 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>
* 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>
* 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>
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>
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>
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>
* 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>
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)
* 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
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>
- 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>
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>
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.
Address adversarial review feedback on PR #3102:
1. shell:true is now conditional (process.platform === 'win32')
- POSIX path unchanged: no shell spawn, no overhead, original
signal/exit-code semantics and windowsHide effect preserved
- Windows path: still routes through cmd.exe to resolve npm.cmd
via PATHEXT (the actual fix for ENOENT)
2. Added .changeset/windows-npm-shell-fix.md (Fixed type)
Reviewed feedback resolved:
- Cross-platform regression risk → shell now Windows-only
- Missing changeset → added
* fix(#3129): replace bypassed bash regex with token-walk git-cmd.js classifier
Root cause: gsd-validate-commit.sh used:
if [[ "$CMD" =~ ^git[[:space:]]+commit ]]
This regex silently bypasses Conventional Commits enforcement for:
git -C /path commit -m ... (working-directory prefix)
GIT_AUTHOR_NAME=x git commit (env-var prefix)
/usr/bin/git commit -m ... (full-path executable)
Fix: introduces hooks/lib/git-cmd.js with isGitSubcommand(cmd, sub) —
a token-walk classifier that handles all four forms by:
1. Skipping leading VAR=VALUE env assignments
2. Validating the git executable (basename check for full-path support)
3. Consuming git global options (-C <path>, --git-dir=, -p, etc.)
4. Checking the subcommand token
The hook delegates to this classifier via node shell-out. node is
already called twice in this hook (config check + JSON parse), so no
new runtime dependency.
This becomes the single source of truth for all hooks that gate on
git subcommands (pre-commit-review-gate, post-push-verify, etc.).
Regression test: 27 assertions — tokenize correctness, 12 must-match
cases (including all 3 bypass forms), 8 must-not-match cases, 3 source
checks. All are real behavioral tests, not string comparisons.
Suite: 7035/7035. Closes#3129.
* fix(lint+hook+changeset): allow-test-rule, fix HOOK_DIR quote injection, fix changeset pr+typo
Without shell:true, execFileSync('npm', ...) on Windows fails with
ENOENT because npm is distributed as npm.cmd, not as a literal 'npm'
binary. The silent try/catch swallows the error, latest stays null,
update_available becomes null, and the statusline never shows
"⬆ /gsd-update" — Windows users miss every release.
Adding shell:true makes execFileSync route through cmd.exe which
resolves npm.cmd via PATHEXT, identical behavior on POSIX.
Repro on Windows:
$env:GSD_CACHE_FILE = "$env:USERPROFILE\.cache\gsd\gsd-update-check.json"
node ~\.claude\hooks\gsd-check-update-worker.js
Get-Content "$env:USERPROFILE\.cache\gsd\gsd-update-check.json"
Before: {"update_available":null,"installed":"1.40.0","latest":"unknown",...}
After: {"update_available":false,"installed":"1.40.0","latest":"1.40.0",...}
* feat(hooks): opt-in SessionStart update banner for non-statusline users (#2795)
When a user declines (or keeps a non-GSD) statusline at install time, the
installer now offers an opt-in SessionStart banner that surfaces GSD update
availability. The banner reads the existing
~/.cache/gsd/gsd-update-check.json cache (written by
gsd-check-update-worker.js) and emits a single systemMessage line only when
update_available is true:
GSD update available: <installed> → <latest>. Run /gsd-update.
It is silent when up-to-date and rate-limits "check failed" diagnostics to
once per 24h via a sentinel file so a corrupt cache doesn't nag every
session. Removed cleanly by `npx get-shit-done-cc --uninstall` which strips
both the script and the SessionStart entry. The banner is never offered when
GSD's statusline is being installed (statusline already surfaces update
info, so re-prompting would be noise).
Implementation:
- hooks/gsd-update-banner.js — pure functions buildBannerOutput,
shouldSuppressFailureWarning, readCache; thin main() wires them.
- bin/install.js — handleUpdateBanner() prompt, parseUpdateBannerInput(),
buildUpdateBannerHookEntry(), buildUpdateBannerPromptText(); chained into
installAllRuntimes() so finalize() receives both flags. updateBannerCommand
computed alongside the other JS-hook commands; finishInstall() registers
the SessionStart entry only when shouldInstallBanner === true and the
hook file is present at the target.
- Hook ships in scripts/build-hooks.js HOOKS_TO_COPY, listed in
MANAGED_HOOKS for stale-detection in gsd-check-update-worker.js, in the
uninstall hook-removal lists in install.js, and in the
rewriteLegacyManagedNodeHookCommands allowlist.
Tests:
- tests/feat-2795-update-banner.test.cjs — 22 tests, structural-IR
assertions on parsed JSON envelopes (no raw-text matching). Covers
pure-function branches (cache present/absent, parseError, rate-limit
suppression, missing version fields), end-to-end hook invocation against
fixture cache states, and install.js wiring (prompt text, input parsing,
hook entry shape).
- tests/trae-install.test.cjs — updated install() return-shape assertion to
include updateBannerCommand: null for the no-settings runtime.
- 6881/6881 tests pass.
Docs (bundled in same commit per the bundle-docs-with-code skill):
- docs/USER-GUIDE.md — new "Surface GSD Update Notifications Without GSD's
Statusline" task section with opt-in/opt-out instructions.
- docs/FEATURES.md — REQ-HOOK-08 added; "Update Banner" subsection under
the Hook System feature with cache flow + removal path.
- docs/INVENTORY.md — hook count 11 → 12, new row for gsd-update-banner.js.
- docs/INVENTORY-MANIFEST.json — regenerated.
Closes#2795
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(install): gate banner prompt on actual installability (CR #3035)
CodeRabbit findings on PR #3035:
- bin/install.js (Major): continueAfterStatusline gated banner prompt on
the raw `shouldInstallStatusline` flag from handleStatusline. But
finishInstall later silently skips the statusline write on local
installs unless --force-statusline is set (#2248). Two consequences:
1. Interactive local Claude/Gemini installs got neither a statusline
nor a banner offer.
2. Codex/Cursor/Copilot/Windsurf/Trae/Cline-only installs (where
every result.updateBannerCommand is null) still got prompted even
though the choice was silently ignored.
Fix: derive willInstallStatusline = shouldInstallStatusline &&
(isGlobal || forceStatusline), and gate the banner prompt on a
canInstallBanner precondition computed from results[].updateBannerCommand.
Pass the raw shouldInstallStatusline through to finalize unchanged so
per-runtime statusline gating in finishInstall is unaffected.
- tests/feat-2795-update-banner.test.cjs (Minor): rate-limit suppression
test parsed r1.stdout without first asserting r1.status === 0. Other
e2e tests in this file (lines 210, 241) do this. A non-zero exit would
surface as a cryptic SyntaxError instead of a status assertion failure.
Fix applied verbatim.
6881/6881 tests pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* test(#2974): migrate 8 test files to typed-IR assertions
Replaces raw stdout/stderr substring matching with structured-field
assertions per CONTRIBUTING.md "Prohibited: Raw Text Matching on Test
Outputs". Adds shared infrastructure for typed error emission so this
pattern is the easy path going forward.
Shared infrastructure:
- core.cjs: ERROR_REASON frozen enum + setJsonErrorMode/getJsonErrorMode
- gsd-tools.cjs: --json-errors CLI flag, parsed before subcommand dispatch
- config.cjs: typed reasons at all 7 error sites
- graphify.cjs: GRAPHIFY_REASON enum + reason/timeout_ms in execGraphify result
- bin/install.js: pure buildSdkFailFastReport() IR builder + renderer
- hooks/gsd-session-state.sh, gsd-phase-boundary.sh: emit Claude Code
hookSpecificOutput JSON envelope with typed state_present/config_mode/
planning_modified/file_path fields (no-op when hooks.community is off)
Test migrations (all pass, 171 tests across the 8 files):
- bug-2649-sdk-fail-fast: assert on ir.reason / ir.context / ir.fix_command
- bug-2687-config-read-warning-parity: assert.equal stderr === ''
- bug-2796-arg-parsing-regression: assert on result.json.updated/.phase
- bug-2838-summary-rescue: parse rescue footer, assert mtime invariant
- bug-2943-config-get-context-window: parse JSON, assert ERROR_REASON.CONFIG_KEY_NOT_FOUND
- graphify: assert reason === GRAPHIFY_REASON.ENOENT/TIMEOUT
- hooks-opt-in: parse hookSpecificOutput, assert typed fields
- security-scan: reclassified as source-text-is-the-product (scan label
output and CI workflow YAML ARE the deployed contract)
Verification: lint-no-source-grep clean (0 violations), full suite
6741/6741 pass.
Closes#2974
* test(#2974): address CR feedback — typed code field, robust idempotency
Two CodeRabbit findings on #3016 addressed:
1. tests/hooks-opt-in.test.cjs:355 (Minor, inline) —
parsed.reason.includes('Conventional Commits') was still substring
matching after the typed-IR migration. Fixed at the source: the
gsd-validate-commit hook now emits a typed `code` field
('CONVENTIONAL_COMMITS_VIOLATION', 'COMMIT_SUBJECT_TOO_LONG')
alongside the human-readable `reason`. Test asserts strictEqual
on the code; the prose copy is no longer part of the test contract.
2. tests/bug-2838-summary-rescue-gitignored-planning.test.cjs:224-250
(Outside-diff) — mtimeMs alone can stay unchanged on coarse-grained
filesystems (HFS+, FAT) when two rewrites land within the same
timestamp tick, falsely passing the idempotency assertion.
Replaced with a full snapshot (mtimeMs, ctimeMs, size, ino, sha256
of contents) compared via assert.deepStrictEqual — the hash
catches any rewrite the timestamp would miss.
Verification: 30/30 pass on the two affected files; lint-no-source-grep
clean (0 violations across 368 test files).
* feat(#2833): parseStateMd reads phase-lifecycle frontmatter fields
Extend parseStateMd() to parse 4 new STATE.md frontmatter fields that drive
the phase-lifecycle status-line proposed in #2833:
- active_phase : phase number when orchestrator is in-flight, null when idle
- next_action : recommended next command when idle
- next_phases : YAML flow array of phase numbers for next_action
- progress : nested block with completed_phases / total_phases / percent
All fields default to undefined when absent — formatGsdState() (next commit)
degrades gracefully so existing STATE.md files keep rendering as before.
YAML scope intentionally narrow:
- Only top-level scalar keys (status, milestone, active_phase, next_action)
- Only single-line flow array for next_phases ([...])
- progress block requires 2-space indent for nested keys
Block sequences (- item over multiple lines) and inline comments inside
nested blocks are NOT parsed — keeping the regex-based parser predictable.
Comments outside frontmatter or after the closing --- still work.
Tests: all 27 existing tests still pass (no behavior change for STATE.md
files that don't carry the new fields).
Refs #2833
* feat(#2833): formatGsdState renders phase-lifecycle scenes + opt-in progress bar
Extend formatGsdState() with three lifecycle scenes that activate when the
new STATE.md frontmatter fields (added in the previous commit) are present.
Also append an opt-in progress bar to the milestone segment when
progress.percent is available.
Scenes (first match wins; falls through to the existing path otherwise):
1. active_phase set → 'v2.0 [██░] X% · Phase 4.5 executing'
(status field carries the lifecycle stage:
discussing / planning / executing / verifying)
2. active_phase null + → 'v2.0 [██░] X% · next execute-phase 4.5'
next_action set (idle state — surfaces what the user should
run next without opening STATE.md)
3. percent=100 (or → 'v2.0 [██████████] 100% · milestone complete'
completed=total)
4. (default fallback) → 'v1.9 Code Quality · executing · ph (1/5)'
(existing rendering, byte-for-byte preserved
when none of the new fields are populated)
Backward compat is the design priority:
- STATE.md files without the new fields render identically to v1.38.x
- progress bar is opt-in (empty string when percent absent)
- Each new scene only activates when its specific fields are populated
A new helper renderProgressBar() generates the 10-segment bar that matches
the existing context meter style (so the two bars on the status-line are
visually consistent).
Tests: 27/27 existing tests still pass.
Refs #2833
* test(#2833): cover parseStateMd lifecycle fields + formatGsdState scenes
26 new tests organized in 5 describe blocks, modeled after the existing
enh-2538-statusline-last-command.test.cjs convention:
parseStateMd #2833 lifecycle fields (7 tests)
- reads active_phase / next_action / next_phases / progress.percent
- 'null' literal handled correctly
- YAML flow array parsing (1 item, multiple items)
- progress nested block (3 fields)
- absent fields return undefined
formatGsdState #2833 lifecycle scenes (6 tests)
- Scene 1: active_phase set → 'Phase X.Y <stage>'
- Scene 2: idle + next_action → 'next <action> <phases>' (1+ phases)
- Scene 3: percent=100 OR completed=total → 'milestone complete'
formatGsdState #2833 backward compatibility (4 tests) — CRITICAL
- Legacy STATE.md (no new fields) renders byte-for-byte unchanged
- Empty state, partial state, progress-bar-opt-in all preserved
progress bar rendering (6 tests)
- 0% / 50% / 100% / clamping / opt-in absence
formatGsdState #2833 scene priority (3 tests)
- active_phase wins over next_action when both populated
- next_action wins over fallback when active_phase null
- percent=100 wins over fallback even with phase set
Combined run: 53/53 tests pass (existing 27 + new 26).
Refs #2833
* docs(#2833): describe phase-lifecycle frontmatter fields and rendering scenes
Add docs/STATE-MD-LIFECYCLE.md as the canonical reference for the four new
STATE.md frontmatter fields and the four status-line rendering scenes
introduced by this proposal:
- Frontmatter field reference (active_phase / next_action / next_phases /
progress.percent) with type and population semantics
- Why progress.percent is intentionally the phase dimension and not the
plans dimension (plans dimension trends optimistic when future phases
are unplanned)
- The four rendering scenes including their priority order
- Stage-label convention for Scene 1 (discussing / planning / executing /
verifying matching the four phase orchestrators)
- Frontmatter parsing constraints — frontmatter must start at file head,
no comments inside nested blocks, next_phases is single-line flow only
- Backward-compatibility guarantee (locked in by the test suite)
- Cross-links to the foundation issue #1989 and the read-side issues
this proposal helps close
The document deliberately scopes itself to the read-side (what the hook
parses, what it renders). Write-side SDK and workflow changes that
auto-maintain the fields are out of scope for this PR so each piece can
be reviewed independently — see the issue thread for the full proposal.
Refs #2833
* test(#2833): simplify '0% renders 10 empty segments' assertion
Address CodeRabbit nitpick — drop the convoluted assert.equal that built
the expected value via .replace() and rely on the existing assert.ok
includes-check. The behavior under test is unchanged; the assertion is
just easier to read.
Refs #2884 review comment
Adds a `statusline.show_last_command` config toggle (default: false) that
appends ` │ last: /<cmd>` to the statusline, showing the most recently
invoked slash command in the current session.
The suffix is derived by tailing the active Claude Code transcript
(provided as transcript_path in the hook input) and extracting the last
<command-name> tag. Reads only the final 256 KiB to stay cheap per render.
Graceful degradation: missing transcript, no recorded command, unreadable
config, or parse errors all silently omit the suffix without breaking the
statusline.
Closes#2538
* fix(hooks): detect Claude Code via stdin session_id, not filtered env (#2520)
The #2344 fix assumed `CLAUDECODE` would propagate to hook subprocesses.
On Claude Code v2.1.116 it doesn't — Claude Code applies a separate env
filter to PreToolUse hook commands that drops bare CLAUDECODE and
CLAUDE_SESSION_ID, keeping only CLAUDE_CODE_*-prefixed vars plus
CLAUDE_PROJECT_DIR. As a result every Edit/Write on an existing file
produced a redundant READ-BEFORE-EDIT advisory inside Claude Code.
Use `data.session_id` from the hook's stdin JSON as the primary Claude
Code signal (it's part of Claude Code's documented PreToolUse hook-input
schema). Keep CLAUDE_CODE_ENTRYPOINT / CLAUDE_CODE_SSE_PORT env checks
as propagation-verified fallbacks, and keep the legacy
CLAUDE_SESSION_ID / CLAUDECODE checks for back-compat and
future-proofing.
Add tests/bug-2520-read-guard-hook-subprocess-env.test.cjs, which spawns
the hook with an env mirroring the actual Claude Code hook-subprocess
filter. Extend the legacy test harnesses to also strip the
propagation-verified CLAUDE_CODE_* vars so positive-path tests keep
passing when the suite itself runs inside a Claude Code session (same
class of leak as #2370 / PR #2375, now covering the new detection
signals).
Non-Claude-host behavior (OpenCode / MiniMax) is unchanged: with no
`session_id` on stdin and no CLAUDE_CODE_* env var, the advisory still
fires.
Closes#2520
* test(2520): isolate session_id signal from env fallbacks in regression test
Per reviewer feedback (Copilot + CodeRabbit on #2521): the session_id
isolation test used the helper's default CLAUDE_CODE_ENTRYPOINT /
CLAUDE_CODE_SSE_PORT values, so the env fallback would rescue the skip
even if the primary `data.session_id` check regressed. Pass an explicit
env override that clears those fallbacks, so only the stdin `session_id`
signal can trigger the skip.
Other cases (env-only fallback, negative / non-Claude host) already
override env appropriately.
---------
Co-authored-by: forfrossen <forfrossensvart@gmail.com>
Bug #2453: After tsc builds sdk/dist/cli.js, npm install -g from a local
directory does not chmod the bin-script target (unlike tarball extraction).
The file lands at mode 644, the gsd-sdk symlink points at a non-executable
file, and command -v gsd-sdk fails on every first install. Fix: explicitly
chmodSync(cliPath, 0o755) immediately after npm install -g completes,
mirroring the pattern used for hook files throughout the installer.
Bug #2451: gsd-context-monitor warning messages over-reported usage by ~13
percentage points vs CC native /context. Root cause: gsd-statusline.js
wrote a buffer-normalized used_pct (accounting for the 16.5% autocompact
reserve) to the bridge file, inflating values. The bridge used_pct is now
raw (Math.round(100 - remaining_percentage)), consistent with what CC's
native /context command reports. The statusline progress bar continues to
display the normalized value; only the bridge value changes. Updated the
existing #2219 tests to check the normalized display via hook stdout rather
than bridge.used_pct, and added a new assertion that bridge.used_pct is raw.
Closes#2453Closes#2451
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add /gsd-spec-phase — Socratic spec refinement with ambiguity scoring (#2213)
Introduces `/gsd-spec-phase <phase>` as an optional pre-step before discuss-phase.
Clarifies WHAT a phase delivers (requirements, boundaries, acceptance criteria) with
quantitative ambiguity scoring before discuss-phase handles HOW to implement.
- `commands/gsd/spec-phase.md` — slash command routing to workflow
- `get-shit-done/workflows/spec-phase.md` — full Socratic interview loop (up to 6
rounds, 5 rotating perspectives: Researcher, Simplifier, Boundary Keeper, Failure
Analyst, Seed Closer) with weighted 4-dimension ambiguity gate (≤ 0.20 to write SPEC.md)
- `get-shit-done/templates/spec.md` — SPEC.md template with falsifiable requirements
(Current/Target/Acceptance per requirement), Boundaries, Acceptance Criteria,
Ambiguity Report, and Interview Log; includes two full worked examples
- `get-shit-done/workflows/discuss-phase.md` — new `check_spec` step detects
`{padded_phase}-SPEC.md` at startup; displays "Found SPEC.md — N requirements
locked. Focusing on implementation decisions."; `analyze_phase` respects `spec_loaded`
flag to skip "what/why" gray areas; `write_context` emits `<spec_lock>` section
with boundary summary and canonical ref to SPEC.md
- `docs/ARCHITECTURE.md` — update command/workflow counts (74→75, 71→72)
Closes#2213
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(hooks): add gsd-read-injection-scanner PostToolUse hook (#2201)
Adds a new PostToolUse hook that scans content returned by the Read tool
for prompt injection patterns, including four summarisation-specific patterns
(retention-directive, permanence-claim, etc.) that survive context compression.
Defense-in-depth for long GSD sessions where the context summariser cannot
distinguish user instructions from content read from external files.
- Advisory-only (warns without blocking), consistent with gsd-prompt-guard.js
- LOW severity for 1-2 patterns, HIGH for 3+
- Inlined pattern library (hook independence)
- Exclusion list: .planning/, REVIEW.md, CHECKPOINT, security docs, hook sources
- Wired in install.js as PostToolUse matcher: Read, timeout: 5s
- Added to MANAGED_HOOKS for staleness detection
- 19 tests covering all 13 acceptance criteria (SCAN-01–07, EXCL-01–06, EDGE-01–06)
Closes#2201
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ci): add read-injection-scanner files to prompt-injection-scan allowlist
Test payloads in tests/read-injection-scanner.test.cjs and inlined patterns
in hooks/gsd-read-injection-scanner.js legitimately contain injection strings.
Add both to the CI script allowlist to prevent false-positive failures.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): assert exitCode, stdout, and signal explicitly in EDGE-05
Addresses CodeRabbit feedback: the success path discarded the return
value so a malformed-JSON input that produced stdout would still pass.
Now captures and asserts all three observable properties.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The autocompact buffer percentage was hardcoded to 16.5%. Users who set
CLAUDE_CODE_AUTO_COMPACT_WINDOW to a custom token count (e.g. 400000 on
a 1M-context model) saw a miscalibrated context meter and incorrect
warning thresholds in the context-monitor hook (which reads used_pct from
the bridge file the statusline writes).
Now reads CLAUDE_CODE_AUTO_COMPACT_WINDOW from the hook env and computes:
buffer_pct = acw_tokens / total_tokens * 100
Defaults to 16.5% when the var is absent or zero, preserving existing
behavior.
Also applies the renameDecimalPhases zero-padding fix for clean CI.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(hooks): stamp gsd-hook-version in .sh hooks and fix stale detection regex (#2136, #2206)
Three-part fix for the persistent "⚠ stale hooks — run /gsd-update" false
positive that appeared on every session after a fresh install.
Root cause: the stale-hook detector (gsd-check-update.js) could only match
the JS comment syntax // in its version regex — never the bash # syntax used
in .sh hooks. And the bash hooks had no version header at all, so they always
landed in the "unknown / stale" branch regardless.
Neither partial fix (PR #2207 regex only, PR #2215 install stamping only) was
sufficient alone:
- Regex fix without install stamping: hooks install with literal
"{{GSD_VERSION}}", the {{-guard silently skips them, bash hook staleness
permanently undetectable after future updates.
- Install stamping without regex fix: hooks are stamped correctly with
"# gsd-hook-version: 1.36.0" but the detector's // regex can't read it;
still falls to the unknown/stale branch on every session.
Fix:
1. Add "# gsd-hook-version: {{GSD_VERSION}}" header to
gsd-phase-boundary.sh, gsd-session-state.sh, gsd-validate-commit.sh
2. Extend install.js (both bundled and Codex paths) to substitute
{{GSD_VERSION}} in .sh files at install time (same as .js hooks)
3. Extend gsd-check-update.js versionMatch regex to handle bash "#"
comment syntax: /(?:\/\/|#) gsd-hook-version:\s*(.+)/
Tests: 11 new assertions across 5 describe blocks covering all three fix
parts independently plus an E2E install+detect round-trip. 3885/3885 pass.
Approach credit: PR #2207 (j2h4u / Maxim Brashenko) for the regex fix;
PR #2215 (nitsan2dots) for the install.js substitution approach.
Closes#2136, #2206, #2209, #2210, #2212
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(hooks): extract check-update worker to dedicated file, eliminating template-literal regex escaping
Move stale-hook detection logic from inline `node -e '<template literal>'` subprocess
to a standalone gsd-check-update-worker.js. Benefits:
- Regex is plain JS with no double-escaping (root cause of the (?:\\/\\/|#) confusion)
- Worker is independently testable and can be read directly by tests
- Uses execFileSync (array args) to satisfy security hook that blocks execSync
- MANAGED_HOOKS now includes gsd-check-update-worker.js itself
Update tests to read worker file instead of main hook for regex/configDir assertions.
All 3886 tests pass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When a user manually installs a dev branch where VERSION > npm latest,
gsd-check-update detects hooks as "stale" and the statusline showed
the red "⚠ stale hooks — run /gsd-update" message. Running /gsd-update
would incorrectly downgrade the dev install to the npm release.
Fix: detect dev install (cache.installed > cache.latest) in the
statusline and show an amber "⚠ dev install — re-run installer to sync
hooks" message instead, with /gsd-update reserved for normal upgrades.
Also expand the update.md workflow's installed > latest branch to
explain the situation and give the correct remediation command
(node bin/install.js --global --claude, not /gsd-update).
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(2136): add failing test for MANAGED_HOOKS missing bash hooks
Asserts that every gsd-*.js and gsd-*.sh file shipped in hooks/ appears
in the MANAGED_HOOKS array inside gsd-check-update.js. The three bash
hooks (gsd-phase-boundary.sh, gsd-session-state.sh, gsd-validate-commit.sh)
were absent, causing this test to fail before the fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(2136): add gsd-phase-boundary.sh, gsd-session-state.sh, gsd-validate-commit.sh to MANAGED_HOOKS
The MANAGED_HOOKS array in gsd-check-update.js only listed the 6 JS hooks.
The 3 bash hooks were never checked for staleness after a GSD update, meaning
users could run stale shell hooks indefinitely without any warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Review feedback from @trek-e — three blocking fixes:
1. **Sentinel prevents repeated firing**
Added warnData.criticalRecorded flag persisted to the warn state file.
Previously the subprocess fired on every DEBOUNCE_CALLS cycle (5 tool
uses) for the rest of the session, overwriting the "crash moment"
record with a new timestamp each time. Now fires exactly once per
CRITICAL session.
2. **Runtime-agnostic path via __dirname**
Replaced hardcoded `path.join(process.env.HOME, '.claude', ...)` with
`path.join(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs')`.
The hook lives at <runtime-config>/hooks/ and gsd-tools.cjs at
<runtime-config>/get-shit-done/bin/ — __dirname resolves correctly on
all runtimes (Claude Code, OpenCode, Gemini, Kilo) without assuming
~/.claude/.
3. **Correct subcommand: state record-session**
Switched from `state update "Stopped At" ...` to
`state record-session --stopped-at ...`. The dedicated command
updates Last session, Last Date, Stopped At, and Resume File
atomically under the state lock.
Also:
- Hoisted `const { spawn } = require('child_process')` to top of file
to match existing require() style.
- Coerced usedPct to Number(usedPct) || 0 to sanitize the bridge file
in case it's malformed or adversarially crafted.
Tests (tests/bug-1974-context-exhaustion-record.test.cjs, 4 cases):
- Subprocess spawns and writes "context exhaustion" on CRITICAL
- Subprocess does NOT spawn when .planning/STATE.md is absent
- Sentinel guard prevents second fire within same session
- Hook source uses __dirname-based path (not hardcoded ~/.claude/)
When the context monitor detects CRITICAL threshold (25% remaining)
and a GSD project is active, spawn a fire-and-forget subprocess to
record "Stopped At: context exhaustion at N%" in STATE.md.
This provides automatic breadcrumbs for /gsd-resume-work when sessions
crash from context exhaustion — the most common unrecoverable scenario.
Previously, session state was only saved via voluntary /gsd-pause-work.
The subprocess is detached and unref'd so it doesn't block the hook
or the agent. The advisory warning to the agent is unchanged.
Closes#1974
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When no in_progress todo is active, fill the middle slot of
gsd-statusline.js with GSD state read from .planning/STATE.md.
Format: <milestone> · <status> · <phase name> (N/total)
- Add readGsdState() — walks up from workspace dir looking for
.planning/STATE.md (bounded at 10 levels / home dir)
- Add parseStateMd() — reads YAML frontmatter (status, milestone,
milestone_name) and Phase line from body; falls back to body Status:
parsing for older STATE.md files without frontmatter
- Add formatGsdState() — joins available parts with ' · ', degrades
gracefully when fields are missing
- Wrap stdin handler in runStatusline() and export helpers so unit
tests can require the file without triggering the script behavior
Strictly additive: active todo wins the slot (unchanged); missing
STATE.md leaves the slot empty (unchanged). Only the "no active todo
AND STATE.md present" path is new.
Uses the YAML frontmatter added for #628, completing the statusline
display that issue originally proposed.
Closes#1989
* fix(hooks): skip read-guard advisory on Claude Code runtime (#1984)
Claude Code natively enforces read-before-edit at the runtime level,
so the gsd-read-guard.js advisory is redundant — it wastes ~80 tokens
per Write/Edit call and clutters tool flow with system-reminder noise.
Add early exit when CLAUDE_SESSION_ID is set (standard Claude Code
session env var). Non-Claude runtimes (OpenCode, Gemini, etc.) that
lack native read-before-edit enforcement continue to receive the
advisory as before.
Closes#1984
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(hooks): sanitize runHook env to prevent test failures in Claude Code
The runHook() test helper now blanks CLAUDE_SESSION_ID so positive-path
tests pass even when the test suite runs inside a Claude Code session.
The new skip test passes the env var explicitly via envOverrides.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The context monitor hook read and parsed config.json on every
PostToolUse event. For non-GSD projects (no .planning/ directory),
this was unnecessary I/O. Add a quick existsSync check for the
.planning/ directory before attempting to read config.json.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move .claude to the front of the detectConfigDir search array so Claude Code
sessions always find their own GSD install first, preventing false "update
available" warnings when an older OpenCode install coexists on the same machine.
Closes#1860
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add stale /gsd: colon reference regression guard
Fixes#1748
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace 39 stale /gsd: colon references with /gsd- hyphen format
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The stale hooks detector in gsd-check-update.js used a broad
`startsWith('gsd-') && endsWith('.js')` filter that matched every
gsd-*.js file in the hooks directory. Orphaned hooks from removed
features (e.g., gsd-intel-*.js) lacked version headers and were
permanently flagged as stale, with no way to clear the warning.
Replace the broad wildcard with a MANAGED_HOOKS allowlist of the 6
JS hooks GSD currently ships. Orphaned files are now ignored.
Regression test verifies: (1) no broad wildcard filter, (2) managed
list matches build-hooks.js HOOKS_TO_COPY, (3) orphaned filenames
are excluded.
Fixes#1750
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#1696
The gsd-prompt-guard.js hook was missing the 'act as a/an/the' prompt
injection pattern that security.cjs includes. Adds the pattern with
the same (?!plan|phase|wave) negative lookahead exception to allow
legitimate GSD workflow references.
* fix(hooks): add read-before-edit guidance for non-Claude runtimes
When models that don't natively enforce read-before-edit hit the guard,
the error message now includes explicit instruction to Read first.
This prevents infinite retry loops that burn through usage.
Closes#1628
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(build): register gsd-read-guard.js in HOOKS_TO_COPY and harden tests
The hook was missing from scripts/build-hooks.js, so global installs
would never receive the hook file in hooks/dist/. Also adds tests for
build registration, install uninstall list, and non-string file_path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace /gsd: command format with /gsd- skill format in all suggestions
All next-step suggestions shown to users were still using the old colon
format (/gsd:xxx) which cannot be copy-pasted as skills. Migrated all
occurrences across agents/, commands/, get-shit-done/, docs/, README files,
bin/install.js (hardcoded defaults for claude runtime), and
get-shit-done/bin/lib/*.cjs (generate-claude-md templates and error messages).
Updated tests to assert new hyphen format instead of old colon format.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: migrate remaining /gsd: format to /gsd- in hooks, workflows, and sdk
Addresses remaining user-facing occurrences missed in the initial migration:
- hooks/: fix 4 user-facing messages (pause-work, update, fast, quick)
and 2 comments in gsd-workflow-guard.js
- get-shit-done/workflows/: fix 21 Skill() literal calls that Claude
executes directly (installer does not transform workflow content)
- sdk/prompt-sanitizer.ts: update regex to strip /gsd- format in addition
to legacy /gsd: format; update JSDoc comment
- tests/: update autonomous-ui-steps, prompt-sanitizer to assert new format
Note: commands/gsd/*.md frontmatter (name: gsd:xxx) intentionally unchanged
— installer derives skillName from directory path, not the name field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(plan-phase): preserve --chain flag in auto-advance sync and handle ui-phase gate in chain mode
Bug 1: step 15 sync-flag check only guarded against --auto, causing
_auto_chain_active to be cleared when plan-phase is invoked without
--auto in ARGUMENTS even though a --chain pipeline was active. Added
--chain to the guard condition, matching discuss-phase behaviour.
Bug 2: UI Design Contract gate (step 5.6) always exited the workflow
when UI-SPEC was missing, breaking the discuss --chain pipeline
silently. When _auto_chain_active is true, the gate now auto-invokes
gsd-ui-phase --auto via Skill() and continues to step 6 without
prompting. Manual invocations retain the existing AskUserQuestion flow.
* fix: remove <sub>/clear</sub> pattern and duplicate old-format command in discuss-phase.md
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(hooks): use semver comparison for update check instead of inequality
gsd-check-update.js used `installed !== latest` to determine if an
update is available. This incorrectly flags an update when the installed
version is NEWER than npm (e.g., installing from git ahead of a release).
Replace with proper semver comparison: update_available is true only
when the npm version is strictly newer than the installed version.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(hooks): use semver comparison for update check instead of inequality
gsd-check-update.js used `installed !== latest` to determine if an
update is available. This incorrectly flags an update when the installed
version is NEWER than npm (e.g., installing from git ahead of a release).
Fix:
- Move isNewer() inside the spawned child process (was in parent scope,
causing ReferenceError in production)
- Strip pre-release suffixes before Number() to avoid NaN
- Apply same semver comparison to stale hooks check (line 95)
update_available is now true only when npm version is strictly newer.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add semver comparison tests for gsd-check-update isNewer()
12 test cases covering: major/minor/patch comparison, equal versions,
installed-ahead-of-npm scenario, pre-release suffix stripping,
null/empty handling, two-segment versions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: explain why isNewer is duplicated in test file
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- fix(#1572): phase complete now marks bold-wrapped plan checkboxes in ROADMAP.md
(`- [ ] **01-01**` format) by allowing optional `**` around plan IDs in the
planCheckboxPattern regex in both phase.cjs and roadmap.cjs
- fix(#1569): manager init no longer recommends 999.x (BACKLOG) phases as next
actions; add guard in cmdManagerInit that skips phases matching /^999(?:\.|$)/
- fix(#1568): add regression tests confirming init execute-phase respects
model_overrides for executor_model, including when resolve_model_ids is 'omit'
- fix(#1533): reject session_id values containing path traversal sequences
(../, /, \) in gsd-context-monitor and gsd-statusline before constructing
/tmp file paths; add security tests covering both hooks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>