Pay down pre-existing error→warn lint debt. Removes dead imports/vars, unused functions, redundant regex/string escapes, and stale eslint-disable directives; converts unused `catch (_e)` to optional catch binding (src/*.cts).
No behavior change. Lint 345→125 warnings (0 errors); deferred categories (n/no-process-exit, test-sleeps, control-regex) tracked in #732 for follow-up. Full test suite green (0 failures); code-review verified all removals unused and all escape fixes semantics-preserving.
Closes#732
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The windows-test-parity ratchet greps test source for fs.rmSync-without-
maxRetries (and six other Windows-portability anti-patterns), failing when an
integer offender COUNT exceeds a frozen baseline (rmSync: 95). A count ratchet
is a Goodhart metric: fixing one offender and adding another keeps the count
constant, so a new defect slips through green. Replace it — and every other
count ratchet in the repo — with a layered, masking-proof design.
Behavioral seam test
- tests/helpers-cleanup.test.cjs proves helpers.cleanup() carries the Windows
EBUSY retry budget. cleanup() delegates retries to Node's fs.rmSync via
maxRetries (it owns no loop), so the test asserts the option contract
(recursive/force/maxRetries>0/retryDelay>0) + real-FS removal + the cwd-guard,
rather than a loop that does not exist. The EBUSY risk is now tested ONCE at
the helper, not approximated textually at every call site.
Write-time ESLint rule (AST-accurate, replaces the grep)
- eslint-rules/no-raw-rmsync-in-tests.cjs (error in tests/**/*.test.cjs) bans
raw fs.rmSync, steering to cleanup(). Catches member, computed (fs['rmSync']),
destructured and aliased forms; escape hatch is inline
`// eslint-disable-next-line local/no-raw-rmsync-in-tests -- <reason>` only.
- Migrated 336 raw fs.rmSync teardown calls across ~116 test files to cleanup().
~18 genuinely load-bearing sites (mid-test SUT/fault-injection removals,
error-swallowing or name-colliding local teardown helpers) keep the raw call
with an inline eslint-disable + reason.
Shared anti-ratchet primitive
- scripts/lib/allowlist-ratchet.cjs:
- assertWithinAllowlist: fails on NOVEL ids (new offender introduced) AND on
STALE ids (a known offender was fixed but not pruned) — identity, not count,
and a ratchet DOWN toward zero.
- assertTightCeiling: a size/length budget whose ceiling must stay within a
grace band of the high-water mark, so budgets may only tighten, never creep.
Ratchets converted onto the primitive
- windows-test-parity-guard.test.cjs: rmSync rule deleted (now ESLint-enforced);
the remaining six patterns moved from integer baselines to named-set
allowlists with ratchet-down.
- scripts/lint-test-file-count.{cjs,allowlist.json}: per-module integer counts →
named filename sets (closes the swap-a-file-keep-the-count blind spot); a
module dropping under cap now FAILS to force pruning its allowlist entry.
- enh-2790 skill-count `<= 63` → named skill allowlist (ratchets toward ~58).
Size budgets hardened (tighten-only)
- agent-size / workflow-size / feat-3039 help-tiered: ceilings lowered to the
current high-water mark and an assertTightCeiling anti-creep check added per
tier. Fixed external-contract limits (description ≤100 chars, agent ≤100 KB)
are intentionally left as-is — they are not grandfathered creeping budgets.
No user-facing behavior change (tests + tooling only); no USER_FACING_PREFIXES
touched, so no changeset fragment is required.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Remove three source-grep describe blocks from bug-1834 and bug-2136 test
files that anchored on byte-offset windows in install.js source. The same
regressions are already fully covered by the E2E behavioral tests (Section 1
in bug-1834, Part 4 in bug-2136) that invoke the actual installer and inspect
the installed files directly.
Co-authored-by: Claude Sonnet 4.6 <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>
Three parallel diagnostic agents (G: sdk/install path-sep, H: hook scripts,
I: worktree/workspace) characterised the remaining windows-22 failures on
23b52f1a. Patches by class:
CRLF in test parsers / file-content reads
- bug-2136-sh-hook-version: shebang check split('\n') → split(/\r?\n/)
- bug-3542-executor-git-stash-prohibition: strip \r from read content
(git rewrites stashed text with CRLF on win autocrlf=true checkout)
- workspace: BOM-strip + explicit \r strip in parseCommandFile (BOM at
byte 0 defeats /^---/ anchor → fmMatch null → fm.name undefined)
Windows path-separator / 8.3-shortname normalization
- bug-3491-nested-git-worktree: use fs.realpathSync.native to expand
%TEMP% RUNNER~1 → runneradmin; normalize sep before path-equality
- prune-orphaned-worktrees: normalize \\→/ before substring includes
(git emits forward-slash in --porcelain on Windows even when
path.join produced backslashes)
- bug-3017-codex-hook-absolute-node: accept POSIX path OR path with
drive-letter prefix in hookPath equality assertion
- bug-3126-global-skills-base-runtime-path: use path.join for expected
/xdg/<runtime> values (production calls path.join → \xdg\… on win32)
Test under-specified platform / forgot win32 env
- bug-2979-hook-absolute-node: pass {platform:'linux'} to
buildHookCommand + rewriteLegacyManagedNodeHookCommands so the
POSIX-branch tests don't pick up the #3393 GitBash code path
- bug-3288-model-catalog + bug-3571-config-manifest: also set
USERPROFILE alongside HOME so os.homedir() on win32 redirects to
the test fixture instead of the runner's real ~
External-cmd resolution
- bug-2647-outer-tarball-sdk-dist: use npm.cmd + {shell:true} on win32
so execFileSync resolves PATHEXT (literal `npm` is ENOENT)
ESM loader: tests/runtime-bridge-sync-smoke.test.cjs already migrated to
pathToFileURL in 23b52f1a (cluster E).
Explicit per-test/describe skip on win32 (with required string reasons
to satisfy no-unconditional-win32-skip guard)
- feat-3347-graphify-auto-update-hook: 3 describes — harness spawns
bash/kill/sleep + the hook itself is bash
- feat-3595-fs-fault-injection: move \t and \n filenames into the
POSIX-only branch (NTFS forbids 0x00–0x1F in filenames)
- bug-2775/2829/3033/3231/3359: POSIX shim under ~/.local/bin with
chmod 0o755 — not how Windows install works
- bug-3211: single subtest where cp.execSync reassignment isn't
picked up on win32 (POSIX coverage via the mock; live windows
behavior covered by 3211-D which keeps running)
- install-path-detection: parses sh-style export PATH= rc files;
Windows has no rc files (registry Path)
- worktree-safety-policy: single test using POSIX /repo/wt fixture
paths that can't be expressed under win32 path.resolve
Validated: plex2 (ubuntu docker) 11224/0 pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The shebang check added in #3105 falls under structural-regression-guard:
the portability constraint (#!/usr/bin/env bash vs #!/bin/bash) cannot
be caught at runtime on distros that have /bin/bash. Adding the annotation
satisfies the adversarial review finding that new tests cannot use
pending-migration-to-typed-ir as their exemption category.
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.
* feat(#2982): extend no-source-grep lint to catch var-binding readFileSync.includes()
The base lint (scripts/lint-no-source-grep.cjs) only catches
readFileSync(...).<text-method>() chained directly. The much more
common var-binding form escapes it:
const src = fs.readFileSync(p, 'utf8');
// 50 lines later
if (src.includes('foo')) {} // ← still grep, lint missed it
Scan of the test suite found ~141 files using this pattern.
Implementation built TDD per #2982 with structured-IR assertions:
scripts/lint-no-source-grep-extras.cjs
- detectVarBindingViolations(src) — pure detector, two passes:
pass 1 collects vars bound from readFileSync, pass 2 finds any
<var>.<includes|startsWith|endsWith|match|search>( on those vars.
- detectWrappedAssertOkMatch(src) — flags
assert.ok(<expr>.match(...)) which escapes the assert.match rule.
- VIOLATION enum exposes stable codes for tests to assert on.
scripts/lint-no-source-grep.cjs
- Wires the new detectors into the existing per-file check; one
additional violation row per file with the first 3 sample tokens.
tests/bug-2982-lint-var-binding.test.cjs
- 13 tests, all assertions on typed VIOLATION enum / structured
records. Covers all 5 text-match methods, multi-var, no-bind,
string literal (must NOT trigger), wrapped assert.ok(.match),
and assert.match (must NOT double-flag).
Migration backlog (#2974 expanded scope):
- 42 files annotated `// allow-test-rule: source-text-is-the-product`
(legitimate — they read .md/.json/.yml files whose deployed text
IS the product)
- 3 files annotated `// allow-test-rule: pending-migration-to-typed-ir [#2974]`
(read .cjs/.js source — clear migration debt)
- 95 files annotated `pending-migration-to-typed-ir [#2974]` with
`Per-file review may reclassify as source-text-is-the-product
during migration` (mixed — manual review under #2974)
After this lands the lint reports 0 violations on main; new
violations in PRs surface immediately.
Closes#2982
Refs #2974
* test(#2982): fix truncated test name per CR
The label ended with a bare '(' from a copy-paste mishap. Now reads
'does NOT flag .matchAll(...) — matchAll is not match, so
assert.ok(.matchAll(...)) is not flagged'.
* chore(#2982): add changeset fragment for PR #2985
* chore(#2982): add changeset fragment for PR #2985
Tests #1834, #1924, #2136 exercise hook/artifact deployment and don't
care about SDK install. Now that installSdkIfNeeded() failures are
fatal, these tests fail on any CI runner without gsd-sdk pre-built
because the sdk/ tsc build path runs and can fail in CI env.
Pass --no-sdk so each test focuses on its actual subject. SDK install
path has dedicated end-to-end coverage in install-smoke.yml.
* 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>