convertClaudeCommandToClaudeSkill(content, skillName, runtime, cmdNames) uses
the runtime arg to gate Hermes/Qwen branding and version: frontmatter emission
(#2808, #3583). Previously the layout module called it with only 2 args so the
runtime-specific formatting was never applied.
Changes:
- skillsKind() gains a runtime param (5th arg after converterName).
- stage() computes cmdNames = readGsdCommandNames() once per call (perf: avoids
repeated fs.readdirSync in the converter) and wraps the real converter so all
4 args are forwarded.
- readGsdCommandNames added to bin/install.js GSD_TEST_MODE exports block so the
stage closure can call it without requiring the script separately.
- All switch arms updated to pass the canonical runtime string.
Converters that do not inspect runtime/cmdNames (Cursor, Codex, Copilot, etc.)
accept and ignore the extra arguments — no behaviour change for those runtimes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(3579): ship graphify hook + lib/ helper through build-hooks + install
`scripts/build-hooks.js` `HOOKS_TO_COPY` did not include
`gsd-graphify-update.sh` (added in #3347 / PR #3557), so it never landed
in `hooks/dist/` and `bin/install.js` — which `readdirSync`s the dist —
never copied it to `~/.claude/hooks/`. The hook's detached rebuild
helper at `hooks/lib/gsd-graphify-rebuild.sh` was also silently dropped
because both build-hooks.js (flat allowlist) and bin/install.js (readdir
+ isFile filter) only walked top-level files.
The published tarball gap (Gap 3 in the issue body) is not reproduced
on origin/main — `npm pack --dry-run --json` shows both source files
are present today. Only Gaps 1 and 2 are in scope.
Changes:
- Add `gsd-graphify-update.sh` to `HOOKS_TO_COPY`.
- Add `HOOKS_SUBDIRS_TO_COPY = ['lib']` and copy whitelisted hook
subdirectories (`hooks/<dir>/*` → `hooks/dist/<dir>/*`) in
build-hooks.js, with the same syntax-check + atomic-rename path the
top-level loop uses.
- `bin/install.js`: when copying `hooks/dist/`, recurse one level into
any directory entry so subdir files (e.g. `lib/gsd-graphify-rebuild.sh`)
land at the mirrored target path the hook's REBUILD_SCRIPT lookup
expects. Top-level if/else structure for files is unchanged.
Regression test `tests/bug-3579-graphify-hook-publish.test.cjs`:
- Drift guard: every top-level `hooks/*.sh` must appear in
`HOOKS_TO_COPY`. Generalizes beyond graphify so the next .sh hook
added cannot regress.
- After build: `hooks/dist/gsd-graphify-update.sh` AND
`hooks/dist/lib/gsd-graphify-rebuild.sh` exist.
- After install: both files land at the target, and no
"Missing expected hook: gsd-graphify-update.sh" warning is emitted.
Fixes#3579
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(3579): replace source-grep drift guard with filesystem-behavior assertion
The Gap-1 drift guard read scripts/build-hooks.js as text and regex-parsed the
HOOKS_TO_COPY literal, which tripped lint-no-source-grep and is brittle under
refactors. Replace with a behavior-based assertion: run the build, then for
every top-level hooks/*.sh assert hooks/dist/<name> exists. Strictly stronger
— catches both the original allowlist gap and any future regression that
silently drops a hook for any other reason.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3406): detect + warn on stale @gsd-build/sdk@0.1.0 global shadow
`@gsd-build/sdk@0.1.0` is the only published version of the standalone
SDK package (the SDK now ships embedded in get-shit-done-cc). When a
user has the stale 0.1.0 globally installed, its `gsd-sdk` bin shadows
the shim get-shit-done-cc wires up — and the 0.1.0 binary only knows
`run | auto | init` (no `query`), so every `gsd-sdk query <cmd>` call
from skills and hooks fails silently until the user runs
`npm uninstall -g @gsd-build/sdk`.
Per maintainer triage decision (option 2): detect at install time and
surface the remediation, instead of waiting for the user to discover
the failure through a broken workflow.
Changes:
- New helper `detectStaleStandaloneSdk(runNpmLs)` (pure function,
accepts an injected executor). Returns `{stale: true, version}` when
`@gsd-build/sdk` is in the top-level dependency tree; returns
`{stale: false}` for every other input including executor throws,
malformed JSON, missing keys, and null/undefined returns.
- New helper `formatStaleStandaloneSdkWarning(info)` — message names
the package, version, the exact `npm uninstall -g @gsd-build/sdk`
remediation command, and references the issue.
- Call site in `install()` for `isGlobal` runs. Spawns
`npm ls -g @gsd-build/sdk --json --depth=0`, recovers the JSON
attached to the non-zero-exit error (npm's "absent" signal),
forwards to detectStaleStandaloneSdk, prints the warning if stale.
Best-effort: any failure is swallowed so detection never blocks
install.
- `GSD_SKIP_STALE_SDK_CHECK=1` opt-out for CI/test environments that
need silence (also used by the install-side test below).
Regression test `tests/bug-3406-stale-sdk-shadow-detect.test.cjs`:
- 8 unit tests pinning every detectStaleStandaloneSdk path (exported,
absent, present, executor-throws, malformed-JSON, no-deps-field,
null/undefined, format).
- 1 install-side end-to-end test confirming that when the package is
absent, the install run does NOT mention `@gsd-build/sdk` or `#3406`
in stdout. Uses a per-test `npm_config_prefix` so the test never
depends on the host's npm dependency tree.
Fixes#3406
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3406): correct changeset pr field — 3406 was the issue number, not the PR
CodeRabbit caught that .changeset/fix-3406-detect-stale-sdk-shadow.md
referenced `pr: 3406` (the issue number) instead of `pr: 3641` (the
PR number). Per CONTEXT.md PRED.k329 changeset frontmatter pr: must
reference the pull request number.
Local: docker gsd-test-summary 11214/0 on plex2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(3406): two CR follow-ups on bin/install.js stale-shadow check
Round-2 CodeRabbit findings on PR #3641:
1. bin/install.js:7769 — GSD_SKIP_STALE_SDK_CHECK opt-out now matches
only explicit "1" / "true" / "yes". The previous any-truthy check
silently disabled the warning for `GSD_SKIP_STALE_SDK_CHECK=0` and
`GSD_SKIP_STALE_SDK_CHECK=false`.
2. bin/install.js:10568 — detectStaleStandaloneSdk now gates stale=true
on version === '0.1.0' (the known-bad shadow). Any newer published
version is intentional and must not flag a "stale shadow" warning
on every install. Added a regression test for non-0.1.0 versions
(1.50.0-canary.0 and 2.0.0) returning stale:false.
Local: 11/11 in the bug-3406 test file + docker gsd-test-summary 11215/0
on plex2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Add first-class grok runtime support (maps to ~/.agents); wire installer, runtime-homes.cjs and sync-skills; update Grok Build engine in local ~/.agents to latest; record session progress in discussion doc
* Normalize gsd colon references to hyphen in generated Claude SKILL.md bodies using the shared transformer. Fixes#3583.
* Refine #3583 implementation after review: cache command names, improve tests, clean up comments
* Harden gsd colon-to-hyphen transformer with bidirectional word boundaries and body-only regression guard
* Track quick-wins batch status and local session notes for #3583/#3579 handoff
* Port installer robustness (hoist copyLibDir + selective Codex hooks) from 3579 to make Codex tests pass on this branch. Fixes ReferenceError and prevents extra hook pollution in Codex installs.
* Restore #3583 transformer wiring and Codex .sh GSD_VERSION branch lost in 50ff8f17 port
Commit 50ff8f17 ('Port installer robustness from #3579') accidentally reverted:
- the top-level require of transformContentToHyphen/readGsdCommandNames
- the body normalization inside convertClaudeCommandToClaudeSkill
- the Codex hook loop's .sh branch with {{GSD_VERSION}} substitution
These were the actual #3583 fix and the Codex half of the #2136 invariant.
Failing tests fixed: bug-2808-skill-hyphen-name, claude-skills-migration #3583
case, bug-2136 Codex .sh substitution.
* Exempt 'sync-skills' slug from docs-parity check (skill dir name in path references)
gsd-sync-skills is an installed Claude skill name and a workflow file but
not a registered slash command. The docs-parity regex catches /gsd-sync-skills
from filesystem path references like ~/.agents/skills/gsd-sync-skills/ in
docs/discussions/grok-build-support-2026-05.md.
Adding to INTERNAL_COMPONENT_SLUGS matches the existing exemption pattern
for 'statusline', 'workspaces', 'graphify-update', etc.
* Restrict hooks/lib/ install to hook-enabled runtimes and managed allowlist
Codex/Copilot/Cursor/Windsurf/Trae/Cline already skip the hooks block but were still copying hooks/lib/ helpers, contradicting the downstream Codex comment. Gate the call on the same runtime check and pass GSD_HOOK_LIB_FILES so install scope matches the uninstall/manifest scope.
`npx get-shit-done-cc@latest --codex` aborted with
"installer migration blocked pending user choice" listing 12 hooks/gsd-*
files. Those files are part of the GSD npm distribution
(hooks/gsd-prompt-guard.js, hooks/gsd-context-monitor.js, etc.), not
user-owned content, so asking the user to choose between keep/remove for
them was a UX bug, not a real choice. The installer is about to write
the fresh bundled versions in their place.
Root cause: `classifyPromptUserAction` in
get-shit-done/bin/lib/installer-migration-report.cjs knew two
unambiguous categories (`stale-sdk-build-artifact`, `user-facing-skill`)
but had no rule for the bundled GSD hooks. The first-time-baseline scan
classified them as `stale-gsd-looking` prompt-user blockers, and
`assertInstallerMigrationsUnblocked` threw.
A second gate compounded the bug: the safe-default resolver in
bin/install.js was wrapped in `if (!_migrationIsTty)`, so even with a
correct classification rule, TTY runs (every `npx get-shit-done-cc`
invocation) skipped the resolver and went straight to the hard throw.
Fix:
1) Add `hooks/gsd-<name>.(js|sh|cjs|mjs)` to `classifyPromptUserAction`
as `bundled-gsd-hook` → `remove`. The regex is anchored at the
top-level `hooks/` directory so nested paths like
`hooks/gsd-helpers/index.js` (or any user-owned helper directory) do
NOT auto-classify.
2) Remove the `!_migrationIsTty` gate from the resolver call in
bin/install.js. The classifier-based path is unambiguous and must
apply regardless of TTY; the env-override branch
(GSD_INSTALLER_MIGRATION_RESOLVE) still applies only when isTty=false
inside the resolver, preserving the #3541 semantic.
Regression test added
(tests/bug-3610-installer-migration-bundled-hooks-classification.test.cjs):
- Positive: hooks/gsd-*.{js,sh} → category=bundled-gsd-hook, choice=remove.
- Counter-test: hooks/my-custom-hook.js → classifier returns null
(user files are preserved).
- Boundary: hooks/gsd-helpers/index.js → classifier returns null
(nested directories don't auto-classify).
- End-to-end: 12 reporter-exact bundled hooks + empty manifest →
resolver clears every blocker, assertInstallerMigrationsUnblocked
does not throw.
Test exercises the real installer-migration code path
(`runInstallerMigrations` + `resolveInstallerMigrationPromptsForNonTty`
+ `assertInstallerMigrationsUnblocked`) — no source-grep, no raw text
matching on outputs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two follow-up adjustments after running the full suite:
1. Reverted the rewriteTomlKeyLines() change. The original
`match.keyRaw || key` fallback respects user ownership of pre-existing
legacy lines (#2760 defensive principle). My fix now applies the
canonical-vs-legacy split at the INSERTION points only: fresh installs
write `hooks = true`, but a pre-existing user-authored
`codex_hooks = true` is preserved verbatim. Codex's own runtime
legacy_key alias handles backward-compat at the Codex layer.
2. Updated 12 test cases in tests/codex-config.test.cjs that pinned the
old fresh-write key. These assertions now expect canonical `hooks` for
fresh-write scenarios; tests covering legacy-line preservation already
pass against the narrowed fix without further edits.
Also updated bug-3566 regression-test cases for the legacy-preservation
path — they now verify that user-owned `codex_hooks` survives an install.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#3566
Codex itself marks codex_hooks as a legacy_key in
codex-rs/features/src/legacy.rs. The canonical current Codex feature flag
under [features] is hooks. The GSD installer was still writing codex_hooks
on every fresh install / reinstall, leaving deprecated config behind on
Codex CLI >= 0.130.0.
Introduces a canonical/legacy split in bin/install.js:
CODEX_HOOKS_FEATURE_KEY = 'hooks'
CODEX_HOOKS_FEATURE_LEGACY_KEYS = ['codex_hooks']
isCodexHooksFeatureKey(key) // recognizes canonical OR any legacy alias
Threaded through:
- ensureCodexHooksFeature(): emits canonical hooks; recognizes legacy
codex_hooks; migrates legacy -> canonical in section, root-dotted, and
block-fallback insertion paths.
- hasEnabledCodexHooksFeature(): accepts either canonical or legacy.
- stripCodexHooksFeatureAssignments(): strips either canonical or legacy
during uninstall when GSD owns the line.
- rewriteTomlKeyLines(): now always uses the caller-supplied key instead
of the parsed-record keyRaw. The old `match.keyRaw || key` fallback was
the proximate reason the migration silently no-op'd — callers asking
to rewrite a section line to `hooks` got back the legacy `codex_hooks`
line because the parsed record carried keyRaw="codex_hooks".
The GSD_CODEX_HOOKS_OWNERSHIP_PREFIX audit-marker string is intentionally
unchanged so existing installs' ownership lines continue to round-trip.
Tests:
- New tests/bug-3566-codex-hooks-feature-canonical-key.test.cjs (6 cases):
fresh install writes hooks; section-form legacy migrated; root-dotted
legacy migrated; user-owned hooks preserved; uninstall removes
GSD-owned canonical; uninstall preserves user-owned hooks.
- Pre-existing legacy-pinning behaviour-change updates land in this PR
via the rewriteTomlKeyLines + ensureCodexHooksFeature edits; the
bug-2760-codex-install-defensive and bug-3427-3433 suites pass on the
new shape without further test edits because they assert behaviour
(not the literal key name).
Docs:
- docs/ARCHITECTURE.md row for Codex notes [features].hooks (canonical,
legacy codex_hooks recognized and migrated forward).
- docs/installer-migrations.md row updated to reflect canonical key
and the new Codex 0.130.0 features.hooks compatibility sentinel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes#3562
Codex CLI 0.130.0 only registers commands from skills/<name>/SKILL.md; it
does NOT auto-discover from get-shit-done/workflows/*.md or agents/*.md.
Prior installer logic (#3427/#3433) removed the gsd-* skill copies under the
assumption that Codex would discover the official skills directly. That
assumption does not hold — users ended up with workflows on disk and zero
$gsd-* entrypoints after restart.
Fix: re-wire copyCommandsAsCodexSkills() (line 5519, already present) into
the Codex install dispatch path (line 8090). Generates one
~/.codex/skills/gsd-<name>/SKILL.md per commands/gsd/*.md — same shape the
Copilot/Antigravity/Cursor/Windsurf/Augment/Trae installs use.
Behaviour change: the pre-existing test in bug-3427-3433-codex-install-shape
asserted "does not regenerate gsd-* skill copies". Updated it to assert
the new behaviour (regenerate gsd-* with refreshed body, preserve non-gsd
user skills).
Tests:
- New tests/bug-3562-codex-install-skill-surface.test.cjs (4 cases):
- skills/gsd-help/SKILL.md exists
- SKILL.md has YAML frontmatter with name: gsd-help
- >= 10 gsd-* skill directories produced (lower-bound, currently 67)
- Pre-existing custom-user-skill directory preserved
- tests/bug-3427-3433-codex-install-shape.test.cjs: updated to assert
regeneration + body refresh + unrelated-skill preservation.
Verified by re-running the issue's repro: `node bin/install.js --codex
--global --config-dir <tmp>` now produces 67 gsd-* skills and the target
~/.codex/skills/gsd-help/SKILL.md exists with valid frontmatter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
migrateCodexHooksMapFormat re-emitted the raw `[hooks.<X>]` path segment as
the leaf TOML key of the new `[[hooks.<EVENT>]]` block. When the legacy
table key was a `<file>:<event>:<line>:<col>` location identifier and the
real event lived in an `event = "..."` body field, the migration emitted a
header like `[[hooks."C:\\Users\\helen\\.codex\\config.toml:session_start:0:0"]]`
that Codex 0.124.0+ refuses to load — causing `npx get-shit-done-cc@latest`
to abort the Codex runtime install on Windows configs that pre-date AoT.
Mirror the flat-AoT branch in the map-format and stale-namespaced-AoT
branches: when the section body declares `event = "..."`, that name wins
as the leaf key and `event` is excluded from the re-emitted handler body.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(skill-deps): add requires: frontmatter to all 51 skills with cross-skill references
Mechanical migration from docs/research/data/2026-05-12-skill-audit.json.
Every skill whose body references another GSD skill now declares those
dependencies in `requires:` YAML frontmatter (flow-style array).
Notable: discuss-phase, plan-phase, and execute-phase all reference `phase`,
which confirms the latent gap in MINIMAL_SKILL_ALLOWLIST — `phase` is pulled
by the core loop but was never in the allowlist. The profile closure model
(ADR-0010 Phase 1) resolves this automatically.
Closes part of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(skill-surface-budget): add PROFILES map, resolveProfile, loadSkillsManifest, staging, marker IO
Implements the Skill Surface Budget Module core (ADR-0010, Phase 1):
- PROFILES Object.freeze map: core (6 skills), standard (~13), full ('*')
- loadSkillsManifest: parses requires: frontmatter from commands/gsd/*.md
into a Map<stem, string[]> without external YAML dep
- resolveProfile({modes, manifest}): computes transitive closure over the
requires: graph; composable (modes=['core','audit'] unions closures)
- stageSkillsForProfile / stageAgentsForProfile: filesystem staging with
same exit-cleanup machinery as the legacy stageSkillsForMode
- readActiveProfile / writeActiveProfile: .gsd-profile marker round-trip
- Back-compat shims preserved: MINIMAL_SKILL_ALLOWLIST, isMinimalMode,
shouldInstallSkill (overloaded), stageSkillsForMode — all legacy tests pass
The phase latent bug is now resolved by closure: discuss-phase, plan-phase,
and execute-phase all require phase, so any profile including any of them
automatically includes phase via transitive closure.
Tests: 22 manifest+resolve, 9 stage, 10 marker (41 new tests, all green).
Back-compat anchor: 80/80 passing.
Closes part of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(skill-surface-budget): add lint-skill-deps.cjs CI gate and fix 19 missed requires: entries
Two lint checks (scripts/lint-skill-deps.cjs):
a) Frontmatter-body consistency: skill body references must appear in requires:
b) Profile closure: every requires: dep of any profile skill must be in closure
Running the lint revealed 19 body references missed by the audit JSON (the
audit used static analysis; some bodies have conditional references). Fixed:
complete-milestone: +audit-milestone, discuss-phase, plan-phase, execute-phase, new-milestone
fast: +quick
health: +thread
map-codebase: +new-project, plan-phase
new-milestone, new-project, review, ultraplan-phase: +plan-phase
ship: +verify-work
sketch, spike: +new-project
verify-work: +execute-phase
workstreams: +new-milestone, resume-work
Wired into package.json as lint:skill-deps and added to pretest.
8 fixture-based tests: all green.
Closes part of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(skill-surface-budget): wire --profile= arg, profile marker write/read in bin/install.js
- Add --profile=<name> / --profile=<n1>,<n2> arg parsing (composable).
Mutually exclusive with --minimal / --core-only (aliases for --profile=core).
Default (no flag): full.
- Import readActiveProfile / writeActiveProfile from install-profiles.cjs.
- After writeManifest: persist active profile to .gsd-profile marker.
- gsd update path: if no --profile flag given, read existing .gsd-profile
marker so non-full profiles are not silently re-expanded to full (ADR-0010).
- Update --help block to document --profile= with per-tier token costs.
New test: install-minimal-backcompat.test.cjs (6 tests):
- PROFILES.core === MINIMAL_SKILL_ALLOWLIST (contract)
- --minimal writes .gsd-profile marker "core"
- --profile=core, --profile=standard write correct markers
- default install writes marker "full"
Closes part of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(changeset): add feat-3408-skill-profiles changelog fragment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(install-profiles): derive agents from skill body refs and wire into resolveProfile
Deviation 1 of ADR-0010 phase 1b: tiered profiles (core, standard) now produce
a non-empty agents Set instead of always returning empty. resolveProfile() scans
each skill body for gsd-* agent name references (via new parseCallsAgents()),
stores them in _calls_agents_<stem> manifest entries, and unions them across the
resolved skill closure. stageAgentsForProfile() already checked resolvedProfile.agents
— it now gets real data so tiered profiles install the correct subset of agents
instead of zero.
Closes#3408 (partial — Deviation 1 only)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(install): honor .gsd-profile marker on update, add resolveEffectiveProfile/mostRestrictiveProfile
Deviation 2 of ADR-0010 phase 1b: the marker written during installation is now
actually honored when re-running without explicit flags (e.g. gsd update). The
dead-end logging block is replaced by resolveEffectiveProfile(), which picks the
marker profile over 'full' when no explicit --profile= flag was given. The resolved
profile is piped through to all 13 stageSkillsForMode dispatch sites (now _stageSkills)
so updates install only the previously-chosen skill subset.
--minimal retains its back-compat behavior (strict 6-skill allowlist, no closure)
while writing 'core' to the marker. mostRestrictiveProfile() is exported for callers
that need to reconcile disagreeing markers across runtimes (smallest skill set wins).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(surface): add CLUSTERS data + state IO module
Add clusters.cjs with 10 named skill groups covering all 66 skills
(verified by surface-clusters.test.cjs). Add surface.cjs with readSurface/
writeSurface atomic IO, resolveSurface, applySurface, and listSurface.
Tests: 17 passing (11 state IO + 6 cluster integrity).
Closes#3408
* docs(adr): add ADR-0011 Skill Surface Budget Module (Phase 1 accepted, Phase 2 amendment)
Records the install-time profile staging decision (Phase 1, landed) and the
runtime /gsd:surface cluster-toggle decision (Phase 2, in flight) as an
amendment. Updates the ADR README index.
Closes#3408
* docs(install-profiles): update module docblock for Phase 2 and ADR-0011
Corrects the ADR reference from 0010 to 0011, documents the three-profile
model and back-compat aliases, adds resolveEffectiveProfile precedence rule,
and notes the companion surface.cjs Phase 2 engine.
* docs(context): add Skill Surface Budget Module canonical entry
Adds the Domain terms entry for the Skill Surface Budget Module covering
both Phase 1 (install-time profiles, .gsd-profile marker) and Phase 2
(runtime /gsd:surface cluster toggles, clusters.cjs, .gsd-surface.json),
per ADR-0011 Consequences requirement.
* feat(surface): add resolveSurface and applySurface engine + tests
Tests cover: profile → surface equivalence, cluster disable/enable,
explicitAdds transitive closure, applySurface file sync (add missing,
remove superseded, preserve non-gsd files), listSurface token cost.
16 new tests passing.
* docs(readme): document --profile= flag and /gsd:surface command
Brief user-facing mention of install profiles (core/standard/full) and the
/gsd:surface slash command in the Commands table. Points to ADR-0011 for details.
* feat(surface): add /gsd:surface slash command runbook
New skill: gsd:surface — runtime profile/cluster toggle without reinstall.
Sub-commands: list, status, profile <name>, disable/enable <cluster>, reset.
Persists state to .gsd-surface.json (independent of .gsd-profile).
Description 96 chars (≤100 limit). lint:descriptions + lint:skill-deps: 0 violations.
* feat(surface): add changeset fragment for /gsd:surface runtime toggle
* feat(surface): add surface skill stem to utility cluster
surface.md is a new skill; add it to the utility cluster so the
surface-clusters.test.cjs coverage invariant stays satisfied.
* docs(adr): fix ADR references to 0011 and record Phase 2 as shipped
ADR-0010 number was already claimed by the file-operation-engine ADR; this
ADR landed as 0011-skill-surface-budget-module.md. Update inline ADR
references in clusters.cjs, surface.cjs, install-profiles.cjs, and the
Phase 2 changeset to ADR-0011. Update the ADR Status section to record
Phase 2 artifacts as shipped on this branch rather than "in progress".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs(research): port skill-surface-budget memo and audit data
ADR-0011 references docs/research/2026-05-12-skill-surface-budget.md and
docs/research/data/2026-05-12-skill-audit.json, which only existed in the
research worktree. Port both onto this branch so the ADR's References
section resolves and reviewers can read the cluster taxonomy (§3.2),
dependency topology (§3.1), and option grading (§4) that justify Phase 1
and Phase 2 decisions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(registration): register surface/clusters in INVENTORY, COMMANDS, and help.md
- surface.md: convert allowed-tools from inline YAML array to block style
(was parsed as a single tool name "[Read, Write, Bash]" by test harness)
- docs/INVENTORY.md: add CLI module rows for clusters.cjs and surface.cjs;
add Commands row for /gsd-surface; bump CLI Modules count 55→57, Commands 66→67
- docs/INVENTORY-MANIFEST.json: add entries for clusters.cjs, surface.cjs,
and /gsd-surface (filename-based command key)
- docs/COMMANDS.md: add ### `/gsd-surface` heading in Configuration Commands
- get-shit-done/workflows/help.md: add /gsd:surface entry in Configuration section
Fixes registration failures introduced by Phase 2 of #3408.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(surface,docs): scrub .claude leakage and escape hypothetical slash tokens
Two PR regressions introduced earlier on this branch:
1. surface.cjs JSDoc comments contained the canonical paths
(~/.claude/commands/gsd, ~/.claude/agents) as example values, which the
cline-install leak regex (~\/\.claude\/(?:get-shit-done|commands|agents
|hooks)) flagged as install-time path leaks. Reworded the docblocks to
describe runtime-resolved paths without literal ~/.claude tokens.
2. The ported research memo proposed hypothetical Option C dispatchers
using slash syntax (/gsd:milestone, /gsd:research). The
docs-parity-live-registry test enforces that every slash-command token
in docs/ resolves to a real command. Rewrote the Option C sketch
without the slash prefix and added a clarifying note that the
dispatchers are illustrative, not shipped.
Targeted tests now pass: tests/cline-install.test.cjs and
tests/docs-parity-live-registry.test.cjs both green.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: remove raw output/source grep in lint tests
* fix: close coderabbit profile and requires issues
* test: align surface token-cost assertion wording
* fix(install): align core profile alias and defer profile marker write
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: cover Windows hook shell drift for Claude (#3413)
* fix: scope Windows hook syntax to Gemini runtime (#3413)
* docs: add changeset for #3413
* docs: set changeset pr for #3413
* fix: route Windows hook formatting through runtime-aware projection seam
* test: cover runtime projection edge cases for Windows hooks
* fix(docs): add shell-command-projection to inventory parity
* docs: align CLI module shipped count after rebase
* test: codex hooks.state.<key> tables must validate as regular tables (#3285 RED)
Drive validateCodexConfigSchema with a fixture containing both [hooks.state]
and [[hooks.SessionStart]] entries. Expect the state tables to pass as regular
tables. Currently fails — validator over-classifies every hooks.* path as AoT.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): treat codex hooks.state as regular table not AoT (#3285)
validateCodexConfigSchema was over-classifying every hooks.* section header
as an event-handler array-of-tables, rejecting the hooks.state.* namespace
that Codex CLI 0.130.0+ uses for per-hook trust persistence.
Fix:
1. Section-header check: carve out `hooks.state` and `hooks.state.*` from
the AoT-required rule — only paths that are neither of those still
require double-bracket form.
2. Parsed-object check: skip the `state` key when iterating Object.entries
(parsed.hooks) so the "must be array" guard does not fire for the trust
namespace object.
All other hooks.<EVENT> validation (SessionStart AoT, handler-field placement)
is unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3289 for #3285
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): codex hooks.state must be regular-table, reject AoT/scalar (CR finding 1)
- migrateCodexHooksMapFormat: exclude hooks.state and hooks.state.* from
legacy-map detection so [hooks.state] is never promoted to [[hooks.state]] AoT
- validateCodexConfigSchema: reject [[hooks.state]] / [[hooks.state.*]] AoT at
section level; reject Array/scalar values at parsed-object level
- Accept only plain-object shape for hooks.state and hooks.state.* (Codex
CLI 0.130.0+ trust-persistence namespace)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: assert codex hooks.state trust entry preserved with original values (CR finding 2)
Strengthen post-install preservation assertion to verify the actual trust
entry key and its enabled/trusted_hash values survive — not just that
hooks.state is an object. Add two validator-reject tests for [[hooks.state]]
and [[hooks.state.foo]] AoT forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: reproduce model-catalog MODULE_NOT_FOUND in install layout (#3288)
Tests A/B/C exercise the install-layout regression introduced by #3230:
- A: confirms the old 3-level __dirname path fails when sdk/shared/ is absent
- B: confirms the new co-located bin/shared/ path resolves correctly (RED)
- C: confirms install() copies model-catalog.json to co-located path (RED)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): copy sdk/shared/model-catalog.json + resolve chain in CJS (#3288)
Two-part fix for the CRITICAL regression introduced by #3230:
Install-side: bin/install.js now copies sdk/shared/model-catalog.json into
get-shit-done/bin/shared/model-catalog.json immediately after the main
get-shit-done/ copy step. Every runtime install (Claude Code, Codex, OpenCode,
Gemini, etc.) now includes this file in the payload.
CJS-side: model-catalog.cjs replaces the brittle single-path require with a
resolve-chain that checks candidates in order:
1. get-shit-done/bin/shared/model-catalog.json (co-located, preferred post-install)
2. sdk/shared/model-catalog.json (source-repo dev path, legacy fallback)
3. GSD_MODEL_CATALOG env override (custom deployments / test harnesses)
When no candidate resolves, throws with a diagnostic listing all tried paths
(PRED.k301 — throw must include candidate paths for debuggability).
REFACTOR audit: three other __dirname traversals in bin/lib/ were inspected:
- core.cjs:1242 (3 levels up → agents/) — safe; agents/ IS copied to targetDir
- profile-output.cjs:547,740 (2 levels up → templates/) — safe; templates/ is
inside get-shit-done/ and IS copied
Only model-catalog.cjs traversed outside the installed payload.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3293 for #3288
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(model-catalog): narrow catch to missing-file errors; clear env in test (#3288)
Two CR findings from PR #3293 review:
1. model-catalog.cjs catch block swallowed ALL errors — malformed JSON,
permission errors, and other real failures were silently absorbed into
the fallback chain. Now only MODULE_NOT_FOUND (with matching path in
message) and ENOENT are treated as recoverable; any other error is
rethrown immediately.
2. test beforeEach saved GSD_EXPLICIT_CONFIG_DIR but didn't clear it —
a CI-set value could leak into install() and redirect the install to
an unexpected directory, making test C non-deterministic. Added
`delete process.env.GSD_EXPLICIT_CONFIG_DIR` to beforeEach.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: reproduce Windows SDK not found after fresh npx install (#3211)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): Windows persistent Path probe + npx-PATH filter on Windows (#3211)
Add getUserShellWindowsPersistentPath() — the Windows counterpart to
getUserShellPath(). Probes the user-level 'Path' registry key via
powershell.exe so the installer can verify gsd-sdk is reachable from
PowerShell/cmd.exe/Git Bash post-install, not just in the transient
npx subprocess PATH.
Wire it into installSdkIfNeeded: on Windows, use the registry-derived
persistent Path (with npx dirs stripped) as the cross-shell reachability
gate, instead of skipping the check entirely. This is the Windows sibling
of the Linux fix in #3249/#3231.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3282 for #3211
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): include Machine+User Path in Windows persistent probe (#3211)
getUserShellWindowsPersistentPath now merges Machine-level and User-level
registry Path entries (matching the effective PATH that PowerShell, cmd.exe,
and Git Bash inherit), instead of reading only User-level. Reading User-only
would produce a false warning when gsd-sdk is installed in a machine-level
bin dir (e.g. C:\Program Files\nodejs).
Addresses CodeRabbit finding on PR #3282.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: reproduce extractFrontmatter LAST-block bug (#3240)
* test: reproduce state.update progress trampling and percent formula (#3242)
Two failing regression tests:
- Bug A: state.update "Last Activity" tramples curated progress.* frontmatter via readModifyWriteStateMd → syncStateFrontmatter
- Bug B: 12 declared ROADMAP phases / 6 realized / 6/6 plans done → percent: 100 instead of 50 (phase-fraction ignored)
* test: reproduce TOML float rejection and partial rollback (#3245)
Two failing regression tests:
1. parseTomlToObject rejects valid Codex TOML floats (tool_timeout_sec = 20.0)
2. Post-install validation failure leaves skills/, agents/, VERSION on disk
despite restoring config.toml — hybrid state after abort
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): accept TOML floats; idempotent codex rollback (#3245)
Two fixes for the Codex install failure introduced by #2760 CR4 finding 3:
1. parseTomlValue now accepts TOML 1.0 float literals (decimals,
exponents, underscore separators, signed). Codex CLI's serde schema
requires f64 for tool_timeout_sec / startup_timeout_sec — the prior
strict-integer-only check was the inverse of what Codex requires,
causing every config with a float to trigger a fatal schema validation
failure. Date/time separators (-/:T/Z) are still rejected.
2. restoreCodexSnapshot is extended into a unified idempotent rollback
that reverts ALL Codex-specific mutations on failure:
- config.toml (existing behavior)
- skills/gsd-* directories (new)
- agents/gsd-*.{md,toml} files (new)
- get-shit-done/VERSION (new)
- orphaned atomic-write temp files (new)
Pre-install state is captured before the first Codex write so the
rollback reflects the true pre-GSD state. Non-gsd-* user content is
untouched. The rollback is safe to call multiple times and before any
snapshots are captured.
Fixes#3245
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: pr=3254 for #3245
* test: fix source-grep lint violation in bug-3242 test (#3242)
Replace content.includes() check with line-by-line parse of STATE.md body.
The lint enforces structural assertions over raw text matching.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: mark #3242 RED tests as todo pending fix (#3242)
The three failing tests are intentional regression tests for bugs in
state.cjs that will be fixed in a separate PR. Mark them { todo: true }
so they don't block CI on this branch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): tighten TOML underscore placement validation (CR finding 1)
The float regex used [\d_]* which accepts invalid forms like 1__0, 1_.0,
and 1._0. TOML 1.0 §2 requires underscores only between digits. Switch
both the integer pre-check and the full float pattern to (?:_?\d)* so
consecutive underscores, leading underscores on a segment, and trailing
underscores on a segment are all rejected before replace(/_/g,'') can
silently normalize them into valid JS numbers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): restore pre-existing gsd-* content on rollback (CR finding 2)
The snapshot only recorded names of pre-existing skills/gsd-* dirs and
agents/gsd-* files. On a failed reinstall the rollback could delete
newly-created dirs but could not restore the bytes of dirs/files that
were overwritten, leaving the user in a hybrid state (old config.toml,
new skill files).
Now snapshot the full file tree of every pre-existing gsd-* skill dir
into codexPreInstallSkillContents (Map<name, Map<relPath, Buffer>>) and
every pre-existing agent file into codexPreInstallAgentContents
(Map<filename, Buffer>). restoreCodexSnapshot() uses these maps to
wipe-and-restore overwritten entries and only removes entries that had
no pre-install state, giving a true atomic rollback guarantee.
Reads are best-effort so a partial snapshot is still better than none.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(install): scope temp-file cleanup to installer-owned writes (CR finding 3)
_cleanTmpFiles() was deleting any *.tmp-<pid>-<n> file found under
targetDir. This is too broad: other tools in the user's Codex/home
directory may create temp files matching the same suffix pattern, and a
GSD install rollback would silently delete them.
Add __atomicWrittenTmps (a module-level Set<string>) populated by
atomicWriteFileSync for every temp path it creates. _cleanTmpFiles()
now checks __atomicWrittenTmps.has(full) before unlinking, so only temp
files this installer process actually wrote are eligible for cleanup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): remove no-op doesNotThrow wrapping try/catch (CR finding 4)
assert.doesNotThrow(() => { try { f(); } catch(_){} }) always passes
because the catch block swallows every exception before the outer
assertion can see it. This meant the rollback-idempotency guarantee was
never actually verified.
Replace with an explicit threw flag around runCodexInstall, assert that
the install did throw (validation failure is expected), and add a
post-rollback state assertion that skills/ was not created. This gives
a loud failure surface if runCodexInstall starts crashing from inside
the rollback path, matching the intent described in the test comment.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): correct describe title for float-acceptance tests (CR nitpick 1)
The describe block title said 'rejects malformed input that previously
slipped through', but the test inside now asserts that TOML floats are
accepted (the #3245 inversion). This misled readers expecting every
sub-test to assert rejection. Update the title to reflect the mixed
behaviour: floats are accepted; dates and trailing-garbage are rejected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): rename test to match what the assertion actually checks (CR nitpick 2)
The test name 'post-install config retains float literal form (20.0 not
truncated to 20)' promised a string-form invariant, but the assertion
uses numeric equality (assert.strictEqual(parsed.tool_timeout_sec, 20))
which cannot distinguish 20 from 20.0 in JS. Rename to 'post-install
config round-trips tool_timeout_sec as numeric 20' so the description
matches what the test actually verifies.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): replace raw text scan with state json assertion (CR nitpick 3)
The 'Last Activity updates the body field' test was reading STATE.md as
raw text, splitting on newlines, and using lines.find/startsWith to
locate the 'Last Activity:' line — the exact pattern-match-on-source
approach prohibited by the no-source-grep testing standard.
Replace with runGsdTools('state json', tmpDir) which surfaces the body-
extracted Last Activity value as fm.last_activity in its JSON output,
and assert against that structured field instead.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): correct post-rollback state assertion for early-failure case
The previous assertion checked that skills/ didn't exist, but the
installer writes skills/ before the schema validator fires. Rollback
removes gsd-* dirs inside skills/, not skills/ itself. Update the
assertion to verify that no gsd-* skill dirs survive rollback, which
is the actual invariant the test name describes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* changeset: document full rollback scope (CR finding 1)
Adds config.toml restoration and orphaned atomic-write temp-file
cleanup to the changeset description — the previous text only listed
skills/, agents/, and VERSION.
* fix(install): wrap post-snapshot scope in rollback handler (CR finding 2)
Any throw between the pre-install snapshot capture and the Codex config
block (skills copy, agents copy, VERSION write, manifest write, leaked-
path scan, etc.) now triggers _codexPreConfigRollback() so the caller
is never left in a partially-installed state. Previously only the later
config.toml mutation paths had rollback wired in.
Introduces _codexPreConfigRollback (defined right after snapshot capture)
and wraps the intervening operations in a try/catch that invokes it on
error for Codex installs; non-Codex paths are unaffected.
* test: assert threw=true to prevent vacuous pass (CR finding 4)
Two tests used bare try/catch without asserting threw === true, so they
would silently pass even if runCodexInstall never threw (k060 pattern).
Each bare catch block is replaced with a threw flag and a
strictEqual(threw, true, ...) assertion.
CR findings 2+3 are both addressed in the preceding install commit:
finding 3 (restore from snapshot manifest, not current FS state) lands
alongside the rollback-wrapper change as part of the restoreCodexSnapshot
refactor.
* fix(install): reject leading zeros in TOML float integer part per TOML 1.0 (CR finding round 4)
TOML 1.0 §2 disallows leading zeros in the integer part of numeric
literals — `01`, `00`, `01.5`, `00e2`, `+01.0`, `-01.0` are all invalid.
The pre-check and float regexes in parseTomlValue used `\d(?:_?\d)*` which
accepted any digit as the leading digit.
Both regexes are tightened to `(0|[1-9](?:_?\d)*)` for the integer part:
- `0` alone is valid
- a non-zero leading digit followed by optional underscored digits is valid
- `01`, `00`, and any variant with a leading zero and further digits is rejected
The "still rejects bare time (07:32:00)" test assertion is broadened from
`/unsupported TOML value/` to `/unsupported TOML value|trailing bytes/`
because the parser now stops at `0` and the remainder `7:32:00` is rejected
as trailing bytes — the invariant (time literals are not accepted) is unchanged.
25 new regression tests cover all rejection cases and valid TOML forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: reproduce false GSD SDK ready signals on Linux (#3231)
* fix(install): require persistent SDK reachability before reporting ready (#3231)
* changeset: pr=3249 for #3231
* fix(install): filter _npx from login-shell PATH probe (CR finding 1)
Apply filterNpxFromPath() to the getUserShellPath() result before passing
it to isGsdSdkOnPath(), mirroring the same filtering already applied to
process.env.PATH. Without this, a transient _npx entry in the login-shell
PATH can falsely satisfy the cross-shell reachability check and reintroduce
the false-ready condition this PR fixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): unconditional legacy-shim replacement assertion (CR finding 2)
Replace readFileSync+includes source-grep check with isLegacyGsdSdkShim()
and add an else branch asserting that when sdkReady is false, a warning/error
was emitted. Previously the sdkReady===false path had no assertion at all,
allowing the test to pass without verifying any postcondition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: replace text-grep assertions with structured ones (CR finding 2 + nitpick)
Finding 2: restructure the legacy-shim replacement assertion to branch on
isLegacyGsdSdkShim() state (a behavioral fact) rather than console output,
and add an unconditional postcondition for both branches.
Nitpick 3 (4 locations):
- lines 149-153: replace /GSD SDK ready/.test(combined) with
isGsdSdkOnPath(filterNpxFromPath(PATH)) === false
- lines 167-169, 185-189: split filterNpxFromPath result into segments array
and use array.includes() instead of string.includes() on the raw PATH string
- lines 375-377: replace /GSD SDK ready/.test(combined) with
fs.existsSync(shimPath) + isGsdSdkOnPath(filterNpxFromPath(localBin))
All 8 tests pass. lint-no-source-grep: 0 violations.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(build-hooks): per-PID staging dir eliminates concurrent-cleanup TOCTOU race
When multiple test before() hooks spawned build-hooks.js concurrently
(--test-concurrency=4), a race existed: Process A would finish all copies,
call rmdirSync('.dist-staging/') in cleanup, then Process B — still in its
copy loop — would call copyFileSync(src, '.dist-staging/hook.pid.ts') and
get ENOENT because the staging directory was gone.
On macOS/Linux, copyFileSync reports the SOURCE path in ENOENT errors when
the destination directory is missing, making the failure appear to be a
missing source file (hooks/gsd-statusline.js) rather than a missing
destination directory. This misled the diagnosis.
Fix: make STAGE_DIR per-PID ('.dist-staging-<pid>/') so each builder owns
its own staging directory. No other process touches it, eliminating all
contention on staging-dir creation and cleanup. Update .gitignore to match
the new 'hooks/.dist-staging-*/' glob.
Reproduces as: CI test matrix (macos-24, ubuntu-22, ubuntu-24) all failing
with ENOENT on hooks/gsd-statusline.js in bug-2136 before() hook. The new
test file added in this PR (bug-3231) shifts the concurrency schedule just
enough to expose the race on every CI run.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: assert on captured console output, not tautological PATH state (CR finding)
The two discarded `captureConsole()` return values in the bug-3231 test
were flagged by CodeRabbit as tautological assertions. Fix:
- Test 1 (transient _npx PATH): capture stdout/stderr and assert the
installer does NOT emit "GSD SDK ready" (the false-positive the PR
fixes), and that it does emit some diagnostic output instead.
- Test 3 (clean install): capture stdout/stderr and assert the installer
DOES emit "GSD SDK ready" after successfully self-linking into a
persistent PATH dir — confirming the positive path works correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>