7424271aa062ee6d7447dfb4b1d3b194e81084a7
228 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e9a66da1e7 |
fix(#2962): write npm-style gsd-sdk shim on Windows under --sdk install (#2971)
* fix(#2962): write npm-style gsd-sdk shim on Windows under --sdk install trySelfLinkGsdSdk previously contained `if (process.platform === 'win32') return null;` — a missed gap from #2775's POSIX self-link rather than an intentional design choice. As a result, `npx get-shit-done-cc@latest --claude --global --sdk` on Windows left `gsd-sdk` off PATH despite the installer reporting success, and the obvious recovery (`npm i -g @gsd-build/sdk`) lands the stale 0.1.0 publication that lacks the `query` subcommand the agents call ~40 times. This PR addresses the shim half. The npm-publish half (publishing @gsd-build/sdk at parity with the get-shit-done-cc version) requires maintainer credentials and is left for separate action. Changes: - bin/install.js: replace the unconditional Windows return-null with dispatch to a new trySelfLinkGsdSdkWindows() that: * resolves npm's global bin via `execFileSync('npm', ['prefix', '-g'])` (no shell interpolation; npm is the only PATH-resolved binary) * verifies write access with a probe before producing partial state * writes the standard npm shim triple to npm's global bin: - gsd-sdk.cmd (cmd.exe; CRLF line endings) - gsd-sdk.ps1 (PowerShell) - gsd-sdk (Bash wrapper for Cygwin/MSYS/Git-Bash) * each shim invokes `node "<absolute path to bin/gsd-sdk.js>"` with the passed args, decoupling shim location from SDK location — same logical structure as the POSIX wrapper-via-require() fallback above * unlinks any stale shims before writing so prior installs don't pin callers to a now-absent path * returns the .cmd path on success (handle the existing onPath check looks for) or null on any failure, falling through to the existing "gsd-sdk is not on your PATH" warning at line 8704 - tests/bug-2962-windows-sdk-shim.test.cjs (new): 5 tests exercising trySelfLinkGsdSdkWindows directly with cp.execFileSync mocked to redirect npm prefix to a temp dir. Asserts shim contents reference the absolute path, .cmd uses CRLF, stale shims are replaced not appended, and null is returned when `npm prefix -g` fails. - tests/no-unconditional-win32-skip.test.cjs (new): regression guard that fails CI if any future commit re-introduces `if (process.platform === 'win32') return null;` (or similar skip-only branches) in bin/install.js. Negative test verified by transiently re-introducing the bad pattern → guard fired → restored → passes. Out of scope: publishing @gsd-build/sdk@<current> to npm so the natural `npm i -g @gsd-build/sdk` recovery also lands a usable SDK. That requires maintainer credentials and is the second half of the issue. Closes #2962 * fix(#2962): address CodeRabbit findings — execSync for npm.cmd, behavior-based regression guard CR finding 1 (🟠 Major): Node's child_process docs explicitly call out that .cmd/.bat files cannot be spawned via execFile/execFileSync without a shell ("Spawning .bat and .cmd files on Windows" section). Since `npm` on Windows is `npm.cmd`, my use of execFileSync('npm', ['prefix', '-g'], { shell: false }) would have failed on the very platform this PR is meant to fix. Switched to cp.execSync('npm prefix -g', ...) — matching the existing convention at line ~8718 which makes the same lookup. Args are static literals so shell interpolation is not an injection vector. CR finding 2 (🟠 Major): the source-grep regression test in tests/no-unconditional-win32-skip.test.cjs violated the repo's no-source-grep testing standard (CONTRIBUTING.md). Replaced with a behavior-based test that: - overrides process.platform to 'win32' via Object.defineProperty - mocks cp.execSync to return a temp-dir as npm prefix - calls trySelfLinkGsdSdk(shimSrc) and asserts it returns non-null AND materializes gsd-sdk.cmd on disk The behavior guard is strictly stronger than the regex version: it would catch any equivalent skip pattern (e.g. os.platform() === 'win32', a typeof-based guard, etc.), not just literal `if (process.platform === 'win32')` text. Negative-tested by re-introducing the `return null` skip → test fails with maintainer-quoted diagnostic "trySelfLinkGsdSdk must not silently return null on Windows; a no-op skip is a missed-parity regression"; restored → passes. Test for Windows shim materialization (bug-2962-windows-sdk-shim.test.cjs) also updated to mock cp.execSync (matching the new production code path) instead of cp.execFileSync. Full suite: 6480/6480 pass. * test(#2962): make Windows shim tests self-contained per CR Each test now invokes trySelfLinkGsdSdkWindows() itself before reading the shim files, so they don't implicitly depend on the earlier test's side effects. Addresses CR's order-dependence finding. * test(#2962): structured shim parsing — eliminate substring source-grep CR found that even after the prior refactor, three tests in the suite still used .includes()/.startsWith() against shim file content (cmdContent.includes(\`@node ${jsonQuoted} %*\`) etc.). Substring matching on file text is the same anti-pattern the no-source-grep standard forbids — even when the file is one this test wrote — because it asserts a literal exists rather than that the structured shape is correct. Replace with three small parsers (parseCmdShim, parsePs1Invocation, parseBashInvocation) that split each shim into header + invocation tokens and assert via deepEqual on structured records. The assertions now check that the .cmd has @ECHO OFF / @SETLOCAL / @node <abs> %* in that order with exactly 3 meaningful lines, and that the .ps1 and bash wrappers split into the expected (call, nodeCmd, target, argToken) tuples. The stale-shim replacement test was hardened the same way: instead of proving the absence of a sentinel substring, it now proves the parsed target equals the new shimSrc and != the old path. Verified: scripts/lint-no-source-grep.cjs reports 0 violations across 348 test files. The 6-test windows-sdk-shim + win32-skip-guard suite all pass. * fix(#2962): expose pure shim IR + tests assert on typed fields, not rendered text Earlier "structured parser" approach (parseCmdShim / parsePs1Invocation / parseBashInvocation) was still raw-text manipulation behind a function wrapper — split('\\r\\n'), trim().split(/\\s+/), content.includes('\\r\\n'). Maintainer was right: hiding grep behind a parser is still grep. Real fix: refactor production code to expose the structured intermediate representation, and have tests assert on the IR fields directly. Production: - New buildWindowsShimTriple(shimSrc) — pure function, no fs/spawn. Returns { invocation: { interpreter, target }, eol: { cmd, ps1, sh }, fileNames: { cmd, ps1, sh }, render: { cmd: () => string, ... } }. The IR is the contract; rendered text is an implementation detail of the renderers. - trySelfLinkGsdSdkWindows now calls buildWindowsShimTriple, looks up filenames from triple.fileNames, and writes triple.render[kind]() to each target. Same observable behavior, structurally separated. - buildWindowsShimTriple added to test-mode exports. Tests (full rewrite — no shim file content is read at any point): - Layer 1: pure-IR tests assert on triple.invocation.target, triple.eol === { cmd: '\\r\\n', ps1: '\\n', sh: '\\n' }, triple.fileNames === { cmd: 'gsd-sdk.cmd', ... }, and the documented IR shape via Object.keys().sort() deepEqual. - Layer 2: fs/spawn driver tests assert filesystem FACTS: - return value equals expected path - all three target files exist as regular non-empty files - rendered file byte length === Buffer.byteLength of triple.render(kind) output (proves the writer writes what the renderer produces, no mutation, no truncation, no double-write — without comparing content) - mtime advances on rewrite (proves stale-replace behavior) - returns null when npm prefix -g throws No more split, .includes, .startsWith, .endsWith, or substring matching anywhere in the test suite. Lint clean. 10/10 tests pass. |
||
|
|
c5dfdbe42e |
fix(#2957): claude+global post-install instructs restart and skill fallback (#2960)
* fix(#2957): claude+global post-install instructs restart and skill fallback `npx get-shit-done-cc --claude --global` writes skills to `~/.claude/skills/gsd-*/SKILL.md` (CC 2.1.88+ format) and removes the legacy `~/.claude/commands/gsd/`. The post-install message still told users to type `/gsd-new-project` without mentioning the required Claude Code restart or the skill-name fallback. On configurations where CC does not auto-surface skills in the slash menu, users hit "no commands appear" and assumed the install failed. Split the post-install message: the existing single-line instruction stays for every non-Claude runtime and for `--claude --local`. For `--claude --global` it now reads: Restart Claude Code, then in any directory either type /gsd-new-project or ask Claude to run the gsd-new-project skill. This covers both invocation paths and surfaces the restart requirement. Add tests/bug-2957-claude-global-postinstall-message.test.cjs as a regression guard: captures the printed message for claude+global, claude+local, and opencode+global; asserts content for each. Verified the test fails on main (pre-fix) and passes after the fix. Closes #2957 * test(#2957): assert legacy generic instruction is replaced not extended CodeRabbit flagged that the test would still pass if the new restart/ fallback copy were printed *alongside* the old 'open a blank directory' instruction. Adding a doesNotMatch assertion proves the claude+global branch replaces the legacy line rather than appending to it. |
||
|
|
7cc6358f91 |
fix(install): honour --minimal across every runtime + manifest fix for Claude local (#2940)
* fix(install): record commands/gsd in manifest for Claude local + per-runtime --minimal coverage writeManifest gated commands/gsd/ recording to Gemini, leaving Claude Code local installs with an incomplete manifest. Audit during #2923 investigation showed every runtime adapter correctly honours --minimal on disk (6 skills, 0 agents) — but Claude local manifest reported 0 skills, breaking saveLocalPatches() drift detection and any downstream tooling that reads manifest.files for the installed surface. Drop the isGemini gate so any runtime that writes commands/gsd/ has those files hashed into the manifest. Adds tests/install-minimal-all-runtimes.test.cjs: spawns the installer end-to-end for all 14 supported runtimes in both --global and --local modes, parses the manifest JSON, and asserts mode === 'minimal', skill set equals MINIMAL_SKILL_ALLOWLIST, and zero gsd-* agents are recorded. Cross-checks the manifest against on-disk skill files. Closes #2923 * test(install): address CR feedback on bug-2923 minimal-runtime tests - Assert installer exit status in runInstall() so failing installs do not produce misleading downstream artifact assertions; include stderr in the failure message for debuggability. - Guard the on-disk vs manifest parity loop with assert.ok(manifest, ...) so the equality check cannot pass accidentally when the manifest is missing. |
||
|
|
372d3453f5 |
fix(install): tokenize before ALL_RUNTIMES_OPTION check + isolate HERMES_HOME in test
Two CodeRabbit findings on PR #2920: 1. parseRuntimeInput previously only matched the bare "16" exactly for the all-runtimes shortcut. Inputs the prompt explicitly encourages — "16,", "16 1", "1,16" — fell through to per-token parsing and silently installed only Claude or a partial subset. Move the ALL_RUNTIMES_OPTION check after tokenization so any token equal to "16" expands. Added regression coverage in tests/multi-runtime-select.test.cjs for the four mixed-input forms. 2. The "maps Hermes to ~/.hermes for global installs" test invoked getGlobalDir('hermes') without isolating HERMES_HOME. On a developer machine that exports HERMES_HOME the assertion would fail even though getGlobalDir was behaving correctly. Save/clear/restore the env var around the assertion, mirroring the pattern the later describe block already uses. Full suite: 6128/6128 pass. |
||
|
|
c9d6306981 |
fix(hermes): rewrite CLAUDE.md → HERMES.md (revert from .hermes.md per spec)
Per the issue spec for #2841 and CodeRabbit feedback on PR #2920, the project-context filename rewrite should produce HERMES.md, not .hermes.md. Reverts the earlier .hermes.md change at all 5 substitution sites in bin/install.js and updates the corresponding regression test in tests/hermes-install.test.cjs to assert HERMES.md. Full suite: 6127/6127 pass. |
||
|
|
bd0511988b |
fix(hermes): nest GSD skills under skills/gsd/ category (#2841)
Per spec in #2841, all 86 GSD skills must collapse into a single "gsd" category in Hermes' system prompt. Previous code passed skills/ as the install root, producing a flat skills/gsd-*/ layout that inflated Hermes' loader output to 86 top-level entries. Changes: - Install path now writes to skills/gsd/{DESCRIPTION.md, gsd-*/SKILL.md} - Uninstall removes the entire skills/gsd/ category dir plus any leftover flat-layout gsd-*/ from older installs (graceful migration) - writeManifest emits skills/gsd/<skill>/<file> paths for Hermes - --skills-root hermes returns the nested category path so /gsd-sync-skills syncs into the right directory - DESCRIPTION.md at category root carries name/version/description so Hermes' skill loader surfaces the GSD category in the system prompt Also extracts promptRuntime's runtimeMap, allRuntimes, parseRuntimeInput, and buildRuntimePromptText to module scope and exports them so tests can assert structurally instead of grepping bin/install.js source. Existing hermes-install tests updated to expect the nested layout and to verify the category DESCRIPTION.md frontmatter (name, version, description) using the shared parseFrontmatter helper. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
4ce72cdee7 |
fix(hermes): align with Hermes Agent conventions per docs review
Four fixes from review of hermes-agent.nousresearch.com docs: 1. SKILL.md frontmatter now declares `version` (required field per Hermes spec). Plumbed through `convertClaudeCommandToClaudeSkill` gated on runtime='hermes' so other runtimes' frontmatter is unchanged. 2. Project-context filename rewrite changed from `HERMES.md` (not discovered by Hermes) to `.hermes.md` (top of Hermes' discovery list: .hermes.md → AGENTS.md → CLAUDE.md → .cursorrules). 3. README + finishInstall now show `/gsd-help` and `/gsd-new-project` for Hermes; per docs, Hermes auto-exposes skills as slash commands. 4. Hermes tests now parse SKILL.md frontmatter structurally via the shared parseFrontmatter helper instead of substring-matching source text, and assert the version/name/description shape required by Hermes' skill_view(). Full suite: 6128/6128 pass (3 new structural assertions). |
||
|
|
b126c0579a |
feat(install): add Hermes Agent runtime support (#2841)
Adds Hermes Agent as a supported installation target. Users can run
\`npx get-shit-done-cc --hermes\` to install all 86 GSD commands as
skills under \`~/.hermes/skills/gsd-*/SKILL.md\`, following the same
open skill standard as Claude Code 2.1.88+, Qwen Code, Antigravity,
Trae, Augment, and Codebuddy.
Hermes Agent is an open-source AI agent framework by Nous Research
(NousResearch/hermes-agent, MIT). Its skill loader accepts the Claude
skill format as-is: frontmatter parsed with PyYAML SafeLoader (unknown
keys like \`allowed-tools\` / \`argument-hint\` ignored), body XML tags
(\`<objective>\`, \`<execution_context>\`, \`<process>\`) passed directly
to the model. Compatibility proven end-to-end with all 86 GSD skills
loading cleanly, \`skill_view()\` returning full bodies, and
\`build_skills_system_prompt()\` emitting them into the agent system
prompt — zero Hermes code changes required.
Changes:
- \`bin/install.js\`: --hermes flag, getDirName/getGlobalDir/getConfigDirFromHome
support, HERMES_HOME env var (native to Hermes — used for profile
mode / Docker deploys), install/uninstall pipelines, interactive
picker option 10 (alphabetical: between Gemini and Kilo), .hermes
path replacements in copyCommandsAsClaudeSkills and
copyWithPathReplacement, legacy commands/gsd cleanup, CLAUDE.md ->
HERMES.md and "Claude Code" -> "Hermes Agent" content rewrites in
skills/agents/hooks, runtime-appropriate finish message.
- \`get-shit-done/bin/lib/core.cjs\`: add hermes to KNOWN_RUNTIMES;
add RUNTIME_PROFILE_MAP.hermes with OpenRouter-slug defaults
(Hermes is provider-agnostic; these defaults resolve across
OpenRouter, native Anthropic, and Copilot via Hermes' aggregator-
aware resolver, and are overridable per-tier via
model_profile_overrides.hermes.{opus,sonnet,haiku}).
- \`README.md\`: Hermes Agent in tagline, runtime list, verification
command, install/uninstall examples, \`--hermes\` flag reference.
- \`tests/hermes-install.test.cjs\`: new, 14 tests covering directory
mapping, HERMES_HOME env var precedence, install/uninstall
lifecycle, user-skill preservation, engine cleanup.
- \`tests/hermes-skills-migration.test.cjs\`: new, 11 tests covering
frontmatter conversion, path replacement (~/.claude/ ->
\$HERMES_HOME/skills/), CLAUDE.md -> HERMES.md, "Claude Code" ->
"Hermes Agent", stale skill cleanup, SKILL.md format validation.
- \`tests/multi-runtime-select.test.cjs\`: updated for new option
numbering (hermes=10, kilo=11, opencode=12, qwen=13, trae=14,
windsurf=15, all=16).
- \`tests/kilo-install.test.cjs\`: updated assertions for Kilo having
moved from option 10 to option 11.
Closes #2841
Implementation notes:
- Zero custom code paths: Hermes reuses copyCommandsAsClaudeSkills()
identical to Qwen Code / Antigravity pattern.
- Path replacement: ~/.claude/, \$HOME/.claude/, ./.claude/ ->
.hermes equivalents in skill/agent/hook content.
- Config precedence: --config-dir > HERMES_HOME > ~/.hermes (matches
how Hermes itself resolves its home directory).
- Legacy cleanup: removes commands/gsd/ if present from a prior
install, preserving dev-preferences.md (same as Qwen).
- No external dependencies added.
Testing: 5841 / 5841 tests pass (0 failures, 0 regressions)
- 14 new tests in hermes-install.test.cjs
- 11 new tests in hermes-skills-migration.test.cjs
- multi-runtime-select.test.cjs renumbered + 1 new test (single choice for hermes)
|
||
|
|
55298b2f70 |
fix(#2876): yamlQuote SKILL.md description for Copilot/Antigravity/Trae/CodeBuddy (#2881)
* fix(#2876): yamlQuote description in Copilot/Antigravity/Trae/CodeBuddy SKILL.md A description starting with `[BETA]` (or any YAML flow indicator — `{`, `*`, `&`, `!`, `|`, `>`, `%`, `@`, backtick) is parsed as a flow sequence/mapping by YAML 1.2-strict loaders. gh-copilot's frontmatter loader fails closed: ✖ ~/.copilot/skills/gsd-ultraplan-phase/SKILL.md: failed to parse YAML frontmatter: Unexpected scalar at node end at line 2, column 21: description: [BETA] Offload plan phase to Claude Code's ultraplan… Six emission sites in `bin/install.js` re-wrote the description without quoting, while the Claude variant (`convertClaudeCommandToClaudeSkill`) already routed it through `yamlQuote`. Brought all six in line: - convertClaudeCommandToCopilotSkill - convertClaudeAgentToCopilotAgent - convertClaudeCommandToAntigravitySkill - convertClaudeAgentToAntigravityAgent - convertClaudeCommandToTraeSkill - convertClaudeCommandToCodebuddySkill Each now wraps the value in `yamlQuote(...)` so any leading character is parser-safe. Regression test (tests/bug-2876-skill-frontmatter-quote.test.cjs) drives the four command converters and two agent converters through the reporter's exact "[BETA] …" description plus a grab-bag of YAML flow indicators, asserting the emitted `description:` value is a quoted YAML scalar. Also round-trips the value through `JSON.parse` for converters that don't apply runtime-name substitution to confirm fidelity. Updated 7 pre-existing substring assertions in copilot-install.test.cjs and antigravity-install.test.cjs that hard-coded the unquoted form. Round trip: 5893/5893 pass on `npm test`. Closes #2876 * test(#2876): structurally parse frontmatter instead of substring-grep Addresses CodeRabbit's two nitpicks on PR #2881: the pre-existing substring assertions in copilot-install.test.cjs (4 sites) and antigravity-install.test.cjs (3 sites) only got bumped from the unquoted form (`description: Diagnose...`) to the quoted-prefix form (`description: "Diagnose...`). Both are still raw-string checks against rendered YAML and drift on any quoting/order change — exactly what the project's CONTRIBUTING.md "no-source-grep" testing standard exists to prevent. Add `parseFrontmatter()` to tests/helpers.cjs — a small parser that handles the YAML scalar forms the install converters emit (double-quoted JSON, single-quoted with `''` escape, bare). Throws if the content has no closed `---` block so a regression in the emitter shape fails loudly rather than silently returning {}. Refactor the 7 description-substring sites to compare on parsed values: the assertion now reads as `fm.description === 'Diagnose planning directory health'` rather than `result.includes('description: "Diagnose planning directory health')`. Same coverage of the #2876 quoting behavior, no coupling to byte-level quote style. `npm test`: 5893/5893 pass. Closes #2876 * test(#2876): make parseFrontmatter delimiter check CRLF/whitespace tolerant CR nitpick on PR #2881 (review at 03:08:08Z): parseFrontmatter() splits on '\n' and compares each line strictly to '---'. A Windows-authored skill file (CRLF endings) leaves a trailing '\r' on every line, so '---\r' fails the equality check, and the helper throws "no closed --- block" on perfectly valid input. Same problem with whitespace-padded delimiter lines. Switch to splitting on /\r?\n/ and comparing the trimmed line. Helper is used by tests/copilot-install.test.cjs and tests/antigravity-install.test.cjs, so this also de-flakes those suites on Windows runners. 5893/5893 on `npm test`. |
||
|
|
4d394a249d |
fix(commands): normalize gsd slash namespace drift (#2858)
* fix(commands): normalize gsd slash namespace drift * fix(#2855): address CodeRabbit findings on namespace drift PR Three CR findings, all valid: 1. autonomous.md line 783 still had `gsd:discuss-phase` (the PR's own normalization missed this line). Switched to `gsd-discuss-phase` and updated the matching test in autonomous-interactive.test.cjs that was asserting the now-retired colon form. 2. tests/bug-2543-gsd-slash-namespace.test.cjs source-grepped the fix-slash-commands.cjs script with .includes() rather than driving its transform behaviour. Refactored fix-slash-commands.cjs to export a pure transformContent(src, cmdNames) function, kept the CLI behaviour unchanged via require.main, and replaced the source-grep block with five behavioural cases: rewrite, multi-occurrence, idempotence on canonical input, no-op on gsd-sdk/gsd-tools, and word-boundary safety. 3. tests/bug-2808-skill-hyphen-name.test.cjs matched `name:` anywhere in SKILL.md; a stray name: in the body could satisfy the assertion. Scoped the lookup to the YAML frontmatter block via the suggested diff (parse the leading --- ... --- region first, then find name: inside it). Full suite: 5854/5854 passing. * fix(#2855): address remaining CodeRabbit findings on PR #2858 Three structural concerns flagged on the namespace-drift fix PR: 1. scripts/fix-slash-commands.cjs:24 — `buildPattern([])` compiled `/gsd:()(?=[^a-zA-Z0-9_-]|$)/g`. The empty capture group still matches any `/gsd:` token followed by a non-word boundary (whitespace, EOL, punctuation), rewriting it to a stray `/gsd-`. Verified live: `transformContent("/gsd:", [])` → `"/gsd-"`. Added a guard returning null from `buildPattern` on empty input and updated `transformContent` and `processDir` to no-op when the pattern is null. 2. tests/autonomous-interactive.test.cjs:44-47 — assertion was `content.includes('gsd-discuss-phase') && content.includes('INTERACTIVE')`, which would false-pass on any unrelated co-occurrence (e.g. `INTERACTIVE=""` initialization plus a stray `gsd-discuss-phase` prose mention). Replaced with a structural extraction: locate the `**If \`INTERACTIVE\` is set:**` branch, bound it by the next `**If` / `<step>` boundary, and assert the `Skill(skill="gsd-discuss-phase", ...)` invocation lives inside that region. Tolerates whitespace around `(`, `skill`, and `=`. 3. tests/bug-2808-skill-hyphen-name.test.cjs:104 — colon-call regex was `Skill\(skill=...` and missed valid formatting like `Skill(skill = "gsd:cmd")` or `Skill( skill = ...)`. Loosened to `Skill\(\s*skill\s*=\s*...` so reformatting drift can't slip past the namespace guard. Verification: 5854/5854 pass on `npm test` from the rebased branch. * fix(#2855): drop pre-validation filter that hid namespace drift CR finding on tests/bug-2808-skill-hyphen-name.test.cjs:128: the test collected generated skill directories with `.filter(entry => entry.isDirectory() && entry.name.startsWith('gsd-'))`, then validated namespace invariants over that filtered list. Anything that violated the prefix invariant — `gsd:extract-learnings` (colon form), `extract_learnings` without prefix, `Gsd-foo` mis-cased — would silently disappear from the iteration and the test would falsely pass. Drop the `startsWith('gsd-')` filter so every generated directory shows up. Add explicit assertions before the existing per-skill loop: - directory list is non-empty (catches a broken converter that produces nothing) - every directory begins with `gsd-` - every directory contains no `:` - every directory contains no `_` Re-audited the full PR diff for the same anti-pattern: only this one site filtered before validating the namespace; bug-2643 and commands-doc-parity also use `readdirSync().filter()` but only by file extension, which is correct. 5854/5854 on `npm test`. * fix(#2855): address remaining CR findings (1 active + 2 nitpicks) Three findings on PR #2858, all the same root cause: input narrowing before validation lets drift slip past the guards. 1. tests/bug-2808-...:104 (active) — `colonCallRe` captured local names with `[a-z0-9-]+`, which excluded the underscore. A drift like `Skill(skill="gsd:extract_learnings")` (deprecated colon syntax with the old underscore filename) silently slid through. Broadened the capture to `[^'"\s)]+` so any malformed local name is surfaced; surrounding pattern (whitespace tolerance, escape support, flags) unchanged. 2. tests/bug-2643-...:43 (nitpick) — `extractSkillNamesHyphen` and `extractSkillNamesColon` had the same over-strict capture plus relied on a single regex over raw bytes, which the project test- rigor memory bans (`feedback_no_source_grep_tests.md`). Replaced with `extractSkillCalls(content)` — a small structural extractor that walks `Skill(` openers, locates each call's matching `)`, parses the body's `skill = "..."` keyword argument with permissive whitespace + quoting + escape handling, and returns `{ name, raw }` records. The two namespace-form helpers become thin filters over the structured output. Tightened the body class to `[^'"\\]+` so a trailing escape `\` before the closing quote (as in `Skill(skill=\"gsd-foo\", …)` written inside another string context) doesn't get included in the captured name. 3. tests/bug-2543-...:44 (nitpick) — `DOC_SEARCH_FILES` was a hand- curated 7-entry array. Every doc added in the future would silently weaken drift detection until someone remembered to extend the list. Replaced with `discoverDocSearchFiles(ROOT)`: globs every `.md` under `docs/` and adds `README.md` if present. New docs are picked up automatically. Re-audited the diff surface for similar narrowings; no other sites filter or constrain before validating namespace invariants. 5854/5854 on `npm test`. * fix(#2855): recurse docs/ tree so localized translations are scanned too CR finding: discoverDocSearchFiles() stopped at docs/*.md, leaving localized translation trees (docs/ja-JP/, docs/zh-CN/, docs/ko-KR/, docs/pt-BR/) and other nested doc collections (docs/skills/, docs/superpowers/) invisible to the namespace-drift invariant. Verified the gap: docs/ has 6 nested directories with ~30 .md files that the previous top-level-only scan was skipping. None contain /gsd: references today, but a future translation update or new doc subdir could leak drift. Switch to an iterative stack walk so every .md under docs/ is scanned regardless of depth. Stack form (rather than recursion) avoids the risk of running into the call-stack limit on deep doc trees. 5854/5854 on `npm test`. --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
73b9d1dac0 |
fix(install): use colon namespace for Gemini slash commands (#2768)
* fix(install): use colon namespace for Gemini slash commands and help reference This fixes unexecutable command recommendations in Gemini CLI by correctly namespacing slash commands (/gsd: instead of /gsd-) in all installed artifacts (agents, commands, workflows). - Implements a lazy command roster discovery to ensure 100% accurate conversion and protect file paths, URLs, and agent names. - Adds isolated behavioral and unit tests covering all boundary cases. - Fixes hardcoded command strings in banners and help output. Closes #2783 * fix(install): close roster gaps in Gemini /gsd- → /gsd: conversion (#2783) Addresses adversarial review findings on PR #2768: - Restore regex boundaries (lookbehind + extension lookahead). Roster-only matching was insufficient: a URL like `https://example.com/gsd-plan-phase` ends in a known command and would be incorrectly converted. Boundaries + roster now agree before any conversion fires. - Smarter trailing lookahead `(?!\.[a-z])` distinguishes file extensions (`.cjs`, `.md`) from sentence-ending punctuation (`.` at end of input or before whitespace), so `/gsd-help.` correctly converts. - Fail loud on missing roster. `commands/gsd/` not found previously fell through to an empty Set, silently no-op'ing every conversion — exactly the bug this code exists to prevent. Now emits a one-shot console.warn (gated on GSD_TEST_MODE) before returning the empty set. - Drop unnecessary `i` flag — GSD commands are always lowercase; matching uppercase tokens against a lowercase roster always misses anyway. - Export `_resetGsdCommandRoster` for test isolation against the module-level cache. Test additions pin the actual safety property of the roster check by using KNOWN command names embedded in URLs and sub-paths — the cases the prior tests didn't reach because they used `gsd-tools` (not in roster). Added a roster-load assertion that fails loudly if the empty-Set fallback path silently neutralises conversions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(install): centralize <sub> stripping and add structural test assertions CodeRabbit findings on the prior commit: - (actionable) Centralizing the Gemini conversion through convertClaudeToGeminiMarkdown dropped the stripSubTags() call that the inline command path used to make before TOML conversion. Move stripSubTags inside convertClaudeToGeminiMarkdown so command/agent/non-command Gemini outputs all have <sub> consistently stripped. Remove the now-redundant stripSubTags call in convertClaudeToGeminiAgent (single source of truth). - (nitpick) Replace `.includes()` checks in the TOML test with structured parsing — JSON-decode each TOML value and assert on parsed fields, per the project's "tests parse, never grep" convention. - (nitpick) Strengthen the install behavioral test to read a real installed artifact (.gemini/commands/gsd/plan-phase.toml), parse it, and assert the prompt body actually contains a /gsd: reference and no unconverted /gsd-plan-phase. A directory-only check would have passed even if every conversion silently no-op'd. - Add a regression test that <sub> tags are stripped through the convertClaudeToGeminiMarkdown pipeline. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
ef08a89241 |
fix(#2866): Codex installer strips legacy hooks at EOF without trailing newline (#2870)
* fix(#2866): Codex installer strips legacy hooks at end-of-file without trailing newline The four shape-strip regexes in `bin/install.js` (Codex install path) required `\r?\n` at end. A stale GSD hook block sitting at end-of-file without a trailing newline (common — many editors strip them, and the legacy installer never wrote one) failed every shape, the installer saw `gsd-check-update` already present, skipped writing the new Nested-AoT block, and Codex 0.125+ refused to load with invalid type: map, expected a sequence in `hooks` Root cause + fix ================ Each shape's terminator changed from `\r?\n` to `(?:\r?\n|$)`, so end-of-file is also a valid terminator. Strip logic was lifted into a new pure helper `stripStaleGsdHookBlocks(configContent)` that the install pipeline now calls in place of the inline replace chain. The helper is exported via the GSD_TEST_MODE module.exports for direct unit-test coverage. Regression test =============== `tests/bug-2866-codex-strip-no-trailing-newline.test.cjs` exercises all four historical shapes (Shape 1 — pre-#1755 gsd-update-check; Shape 2 — flat [[hooks]]+gsd-check-update; Shape 3 — single [[hooks.SessionStart]] without nested .hooks; Shape 4 — correct two-block nested) twice each: once with a trailing newline (regression guard against the existing behavior) and once at end-of-file without a trailing newline (the reporter's exact repro). It also asserts: - the helper is a no-op when no GSD reference is present, and - Shape 4 strip does not leave an orphaned [[hooks.SessionStart]] header behind (the same ordering invariant the inline code relied on). The helper is loaded via `package.json` `bin` field, not a hardcoded path — `tests/bug-2866-codex-strip-no-trailing-newline.test.cjs` parses package.json and resolves `pkg.bin['get-shit-done-cc']` to require the installer. Closes #2866 * test(#2866): assert TOML structure, not raw-text substrings CodeRabbit caught the strip assertions using `.includes()` against raw TOML output. Added a small line-structural parseTomlShape() helper (table headers + dotted-path key/value map, comments stripped) and rewrote the assertions to: - Verify no [[hooks.* table header survives the strip - Verify no key carries a stale gsd-(update|check)-(check|update) value - Verify history.persistence is preserved as the parsed string "save-all" Behaviour is unchanged (the strip function under test is not modified). The assertions now check structural shape rather than substring presence, which catches re-shaping regressions that text matching would miss. No new dependencies — the parser is local to the test and handles only the small well-formed TOML these tests construct. * refactor(#2866): replace regex hook strip with TOML AST removal Per CR feedback on PR #2870: the regex-driven `stripStaleGsdHookBlocks` implementation was fragile to whitespace, indentation, and key-ordering variations the regression test never exercised. Variations the regex silently leaked (verified before the rewrite): - Shape 4 with an extra blank line between parent/child tables - Shape 2/3 with `command` ordered before `event` - Shape 3 with an extra `timeout = 5000` key — worse than a leak: the regex matched only the command line, leaving `timeout = 5000` orphaned outside any TOML table (invalid TOML) - Tight whitespace `event="SessionStart"` (no spaces around `=`) The structural rewrite uses the TOML parser already present in this file (`getTomlTableSections` + `getTomlLineRecords` + `parseTomlValue` + `removeContentRanges` + `collapseTomlBlankLines`): 1. Find every section whose path is `hooks` or starts with `hooks.`. 2. For each, walk the section's line records and parse `command` values structurally — match by basename equality (`gsd-update-check.js` or `gsd-check-update.js`), never by regex on raw bytes. 3. Detect orphaned `[[hooks.SessionStart]]` parents: empty body and a stale child immediately follows → mark for removal. 4. Extend each removal range backward through any preceding `# GSD Hooks` marker line (detected via line records, not text scan). 5. Remove ranges atomically and collapse resulting blank-line runs. Legacy hook basenames are hoisted to template-literal constants so the existing `install-hooks-copy.test.cjs` quoted-literal guard continues to catch accidental *registration* of the inverted filename, while strip detection (which legitimately needs both names) bypasses it. Test coverage added: 8 new sub-tests exercising the four whitespace/ ordering variations (with and without trailing newline) plus a `[[hooks.UserPromptSubmit]]` user-authored hook to guarantee the strip only touches GSD-managed sections. 20/20 in the file, 5867/5867 in the full suite. |
||
|
|
12b6ba4e34 |
fix(#2829): gsd-sdk resolvable in local-mode installs (#2848)
* fix(#2829): gsd-sdk resolvable in local-mode installs Local-mode installs previously short-circuited installSdkIfNeeded() the moment opts.isLocal was true, leaving every `gsd-sdk query …` call site unable to resolve the binary on PATH. The published tarball ships sdk/dist/cli.js and bin/gsd-sdk.js regardless of mode, and the shim resolves the CLI relative to its own __dirname — so the same self-link strategy that powers npx-cache global installs (#2775) also works for local installs. We now run the shared self-link path whenever the dist is present, and only fall back to a non-fatal warning + early return when the dist is genuinely missing (preserving the #2678 contract). * test(#2829): correct precondition comment about ~/.local/bin Address CodeRabbit feedback — the test does not create ~/.local/bin, so reword the inline precondition to "any HOME bin candidate remains off-PATH" to match what the test actually sets up. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
a7f83ee663 |
fix(#2831): expand HOME in OpenCode @file references on all platforms (#2842)
* fix(#2831): expand HOME in OpenCode skill/template paths OpenCode does not shell-expand $HOME in @file references on any platform — the literal `@$HOME/...` path is resolved relative to the config command/ dir, producing `command/$HOME/...` (file not found). The previous fix for #2376 only guarded Windows; extend to all platforms. Closes #2831 * test(#2831): assert behavior via exported computePathPrefix, not source grep Addresses CodeRabbit review on PR #2842: - Extracts pathPrefix logic into a named, test-exported computePathPrefix helper in bin/install.js (no behavior change at the call site). - Rewrites bug-2376 and bug-2831 regression tests to call the exported function directly instead of regex-matching install.js source text, per the repo's no-source-grep testing standard. - Wraps temp-dir test setup in try/finally so cleanup runs on assertion failures (no leaked tmp dirs). |
||
|
|
c3a42d66f9 | Revert "feat(install): add Hermes Agent runtime support" (#2849) | ||
|
|
5a636bc90a |
feat(install): add Hermes Agent runtime support (#2841)
Adds Hermes Agent as a supported installation target. Users can run
\`npx get-shit-done-cc --hermes\` to install all 86 GSD commands as
skills under \`~/.hermes/skills/gsd-*/SKILL.md\`, following the same
open skill standard as Claude Code 2.1.88+, Qwen Code, Antigravity,
Trae, Augment, and Codebuddy.
Hermes Agent is an open-source AI agent framework by Nous Research
(NousResearch/hermes-agent, MIT). Its skill loader accepts the Claude
skill format as-is: frontmatter parsed with PyYAML SafeLoader (unknown
keys like \`allowed-tools\` / \`argument-hint\` ignored), body XML tags
(\`<objective>\`, \`<execution_context>\`, \`<process>\`) passed directly
to the model. Compatibility proven end-to-end with all 86 GSD skills
loading cleanly, \`skill_view()\` returning full bodies, and
\`build_skills_system_prompt()\` emitting them into the agent system
prompt — zero Hermes code changes required.
Changes:
- \`bin/install.js\`: --hermes flag, getDirName/getGlobalDir/getConfigDirFromHome
support, HERMES_HOME env var (native to Hermes — used for profile
mode / Docker deploys), install/uninstall pipelines, interactive
picker option 10 (alphabetical: between Gemini and Kilo), .hermes
path replacements in copyCommandsAsClaudeSkills and
copyWithPathReplacement, legacy commands/gsd cleanup, CLAUDE.md ->
HERMES.md and "Claude Code" -> "Hermes Agent" content rewrites in
skills/agents/hooks, runtime-appropriate finish message.
- \`get-shit-done/bin/lib/core.cjs\`: add hermes to KNOWN_RUNTIMES;
add RUNTIME_PROFILE_MAP.hermes with OpenRouter-slug defaults
(Hermes is provider-agnostic; these defaults resolve across
OpenRouter, native Anthropic, and Copilot via Hermes' aggregator-
aware resolver, and are overridable per-tier via
model_profile_overrides.hermes.{opus,sonnet,haiku}).
- \`README.md\`: Hermes Agent in tagline, runtime list, verification
command, install/uninstall examples, \`--hermes\` flag reference.
- \`tests/hermes-install.test.cjs\`: new, 14 tests covering directory
mapping, HERMES_HOME env var precedence, install/uninstall
lifecycle, user-skill preservation, engine cleanup.
- \`tests/hermes-skills-migration.test.cjs\`: new, 11 tests covering
frontmatter conversion, path replacement (~/.claude/ ->
\$HERMES_HOME/skills/), CLAUDE.md -> HERMES.md, "Claude Code" ->
"Hermes Agent", stale skill cleanup, SKILL.md format validation.
- \`tests/multi-runtime-select.test.cjs\`: updated for new option
numbering (hermes=10, kilo=11, opencode=12, qwen=13, trae=14,
windsurf=15, all=16).
- \`tests/kilo-install.test.cjs\`: updated assertions for Kilo having
moved from option 10 to option 11.
Closes #2841
Implementation notes:
- Zero custom code paths: Hermes reuses copyCommandsAsClaudeSkills()
identical to Qwen Code / Antigravity pattern.
- Path replacement: ~/.claude/, \$HOME/.claude/, ./.claude/ ->
.hermes equivalents in skill/agent/hook content.
- Config precedence: --config-dir > HERMES_HOME > ~/.hermes (matches
how Hermes itself resolves its home directory).
- Legacy cleanup: removes commands/gsd/ if present from a prior
install, preserving dev-preferences.md (same as Qwen).
- No external dependencies added.
Testing: 5841 / 5841 tests pass (0 failures, 0 regressions)
- 14 new tests in hermes-install.test.cjs
- 11 new tests in hermes-skills-migration.test.cjs
- multi-runtime-select.test.cjs renumbered + 1 new test (single choice for hermes)
|
||
|
|
5fe1f00a0d |
fix(#2808): SKILL.md files use hyphen name form (gsd-cmd not gsd:cmd) (#2819)
* fix(#2808): SKILL.md name uses hyphen form for Claude Code autocomplete skillFrontmatterName() was converting gsd-<cmd> to gsd:<cmd> (colon) so installed SKILL.md files had name: gsd:add-phase etc. Claude Code surfaces this name in autocomplete, showing the deprecated colon form to users even though the hyphen form is canonical everywhere else. Root cause: the colon form was needed because workflows called Skill(skill="gsd:<cmd>"). All 4 remaining colon-form Skill() calls in autonomous.md and execute-phase.md are updated to hyphen form. skillFrontmatterName() now returns the hyphen dir name unchanged. Updated 4 existing tests that asserted colon form. Regression test: tests/bug-2808-skill-hyphen-name.test.cjs * fix(#2808): address CodeRabbit — bash/text fences, structured test assertions, fail-loud on errors |
||
|
|
055b43054f |
fix(#2794): embed model_profile_overrides.opencode.<tier> into generated OpenCode agents (#2822)
* docs: add CHANGELOG entry and rc.5 release notes for #2809 Codex hooks migrator fixes Covers the five correctness findings addressed in the round-5 CR of PR #2809: parseHooksBody key parser (hyphenated/quoted keys), buildNestedBlock empty-handler guard, legacyMapSections segment-count filter, quoted-dot regression test, and strengthened command path assertion. Closes #2810 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#2794): embed model_profile_overrides.opencode.<tier> into generated OpenCode agents OpenCode agent files were missing `model:` frontmatter when the user configured tier-based model resolution via `model_profile_overrides.opencode.*`. Only explicit `model_overrides[agent]` was consulted; the runtime profile resolver (used by the Codex path since #2517) was never called for OpenCode agents. Added a tier-resolver fallback in the OpenCode agent conversion block in `bin/install.js`. Precedence (matching Codex behavior): model_overrides[agent] > model_profile_overrides.opencode.<tier> > omit Regression test: `tests/bug-2794-opencode-model-profile-overrides.test.cjs` Closes #2794 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
3c03a153a5 |
fix(#2773): emit correct Codex 0.124.0+ two-level nested hooks schema (#2809)
* fix(#2773): emit correct Codex 0.124.0+ two-level nested hooks schema Codex 0.124.0's stable spec requires: [[hooks.SessionStart]] ← event entry (optional matcher) [[hooks.SessionStart.hooks]] ← handler sub-table type = "command" command = "node ..." Previous GSD versions wrote the flat [[hooks]] + event = "SessionStart" form (#2637) or a single-block [[hooks.SessionStart]] without the nested .hooks sub-table (#2760). Both are rejected by Codex 0.124.0+ at launch. Changes: bin/install.js - Hook block emission now always writes the two-level nested AoT form. - migrateCodexHooksMapFormat extended to also migrate flat [[hooks]] array-of-tables entries (event = "..." key → [[hooks.<EVENT>]] form). Flat [[hooks]] and [[hooks.<EVENT>]] are mutually exclusive TOML types; any pre-existing flat entries must be promoted before GSD appends its own namespaced hooks. - Migrated flat AoT blocks are inserted BEFORE the GSD marker so they stay in the "user" portion of the file and survive stripGsdFromCodexConfig. - stripCodexGsd* regexes cover all four historical block shapes. - validateCodexConfigSchema no longer rejects flat [[hooks]] at the root level (removing the false-positive that blocked install when users had their own AfterCommand hooks). The validator still enforces the nested [[hooks.<EVENT>.hooks]] shape for entries that have a .hooks sub-table. tests/ - bug-2760-codex-install-defensive.test.cjs: 29/29 passing. Added 5 new regression cases for fresh install, upgrade from each legacy shape, idempotent reinstall, and user hook preservation. - codex-config.test.cjs: 106/106 passing. All migration tests updated to assert [[hooks.<TYPE>.hooks]] sub-table (command now in handler level, not event-entry level). New tests: flat [[hooks]] migration (SessionStart, AfterCommand), install+uninstall preserves non-GSD AfterCommand hook. Closes #2773 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address CodeRabbit review + CI regression in bug-2698-crlf-install CI regression (#2698 tests): Strip GSD-managed hook blocks BEFORE running migrateCodexHooksMapFormat. The previous order let migration convert the stale [[hooks]] + event = "SessionStart" + gsd-update-check.js block to [[hooks.SessionStart]] form before Shape 1 strip regex could match it; Shape 1 only matches the flat [[hooks]] form, so the stale block survived reinstall. Swapping to strip-then-migrate ensures only user-authored hooks reach the migration step. Shape 3/4 regexes also extended to match both gsd-check-update.js and the legacy gsd-update-check.js filename so no variant slips through. CodeRabbit actionable (major): migrateCodexHooksMapFormat now accepts single-quoted TOML event values (event = 'SessionStart') in the flat [[hooks]] filter and event-name extractor. TOML spec allows single-quoted literal strings; double-quote-only regexes silently skipped them, leaving the block unmigrated and triggering the hard-fail validator. CodeRabbit nitpicks: tests/codex-config.test.cjs: replace indexOf('[[hooks.AfterCommand]]') ordering check with parseTomlToObject structural assertions (no-source-grep rule). tests/bug-2760-codex-install-defensive.test.cjs: replace three content.match(/…/g).length raw-text counts with parseTomlToObject structural assertions for single-handler and single-event-entry invariants. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address CodeRabbit review #2 — extractFlatHookEventName helper + type assertions - bin/install.js: consolidate TOML_QUOTED_STRING + TOML_EVENT_CAPTURE into a single extractFlatHookEventName() helper that rejects empty-string event values (event = "" or event = ''); previously two independent regexes had to be kept in sync and neither guarded against a blank event name producing a [[hooks.]] header - tests/bug-2760-codex-install-defensive.test.cjs: add comments explaining why the e.command fallback is retained in both allSessionStartCommands and afterToolCommands collectors — migration only upgrades [hooks.TYPE] map-format sections, not existing [[hooks.TYPE]] namespaced AoT entries authored with command at event-entry level; removing the fallback causes false failures for preserved user entries - tests/codex-config.test.cjs: add type = "command" assertions to all migration tests that verify .command but were missing .type checks; buildNestedBlock injects type = "command" when the source body has no explicit type key, so every migrated handler must carry it per the Codex 0.124.0+ schema 138 tests pass, 0 fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: CR round 3 + proactive audit — TOML quoting, stale AoT migration, strict validator Three real issues from CodeRabbit round 3, plus the collateral improvements they enable: bin/install.js — tomlBareKey() helper (#2773 CR6a) buildNestedBlock interpolated the raw event name into [[hooks.${type}]] and [[hooks.${type}.hooks]] headers without TOML escaping. An event name containing spaces or punctuation (e.g. "Before Tool") would produce invalid TOML that parseTomlToObject would subsequently reject. Added tomlBareKey() — wraps the key in double-quoted TOML strings when it contains non-bare-key characters ([A-Za-z0-9_-]). bin/install.js — staleNamespacedAotSections migration path (#2773 CR6b) migrateCodexHooksMapFormat handled [hooks.TYPE] (map-format) and flat [[hooks]] with event = "..." but ignored [[hooks.TYPE]] AoT entries that carried handler fields (command, type, timeout, statusMessage) at event-entry level without a nested [[hooks.TYPE.hooks]] sub-table. This is the pre-#2773 single-block shape that Codex 0.124.0+ rejects. Added staleNamespacedAotSections as the third migration category: detected by STALE_HANDLER_FIELD_PATTERN + absence of a [[hooks.TYPE.hooks]] sub-table in the same file; promoted to the two-level nested form by buildNestedBlock. Matcher-only entries (no handler fields) are intentionally skipped. bin/install.js — validator now rejects event-level handler fields (#2773 CR6c) With migration covering the stale AoT shape, validateCodexConfigSchema can be strict: entries that have handler fields at event-entry level but no .hooks sub-array return ok: false instead of silently passing. Matcher-only entries (no handler fields and no .hooks) remain valid as event filters. tests/codex-config.test.cjs — four new migration tests + missing type assertion Four tests cover the new stale AoT migration path: single-entry promotion, already-nested entry is left untouched (no double-wrap), multiple event types, and matcher-only entry is skipped. Added the missing type = "command" assertion to the CRLF migration test (the one miss from CR round 2). tests/bug-2760-codex-install-defensive.test.cjs — strict .hooks-only collectors With stale AoT entries now migrated, the entry.command fallbacks in allSessionStartCommands and afterToolCommands are dead code. Replaced with strict entry.hooks-only collection guarded by an every(Array.isArray(e.hooks)) pre-assertion, so any future regression that leaves handler fields at event level produces an explicit test failure rather than silently collecting them. 142 tests pass, 0 fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: CR round 4 — segment-safe quoted-key detection + structural test assertions bin/install.js — getTomlTableSections now exposes segments (#2773 CR7a) The staleNamespacedAotSections filter used section.path.split('.').length > 2 to skip [[hooks.TYPE.hooks]] sub-table entries. That check misclassifies quoted event names containing dots: [[hooks."before.tool"]] has path hooks.before.tool (3 dot-parts) but only 2 true parsed segments, so it was incorrectly excluded from migration. Fixed by adding segments to the getTomlTableSections return shape (already available on record.tableHeader.segments) and replacing the split-based check with section.segments.length !== 2, which uses the true parsed key count regardless of dots inside quoted names. tests/codex-config.test.cjs — replace raw-equality assertions (#2773 CR7b) The two new no-op migration tests (already-nested and matcher-only) used assert.strictEqual(result, content) — raw string equality that conflicts with the repo no-source-grep testing standard. Replaced with structural assertions using parseTomlToObject: the already-nested test verifies the handler stays under .hooks[0] and no double-wrap occurs; the matcher-only test verifies the matcher key is preserved and no .hooks sub-array is added. 142 tests pass, 0 fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: CR round 5 — parseHooksBody key parser, empty-handler guard, segment-safe legacyMap filter, stronger test assertions - parseHooksBody: replace /^([\w.]+)\s*=/ regex with parseTomlKey() so hyphenated keys (status-message) and quoted keys are not silently dropped - buildNestedBlock: guard against handlerEntries.length === 0 — do not synthesise [[hooks.TYPE.hooks]] with type="command" but no command for matcher-only or otherwise handler-empty stale sections - legacyMapSections filter: use section.segments.length === 2 (same fix applied to staleNamespacedAotSections in round 4) to prevent [hooks.X.Y] 3-segment tables from being misclassified as event entries - tests: add regression test for [[hooks."before.tool"]] quoted-dot event names; strengthen command path assertion to exact absolute path comparison Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
b44482cf03 |
fix(#2760): defensive Codex install — strip legacy agents blocks, default hooks to AoT, validate post-write schema (#2785)
* fix(#2760): defensive Codex install — strip legacy agents blocks, default hooks to AoT, validate post-write schema Three defects, three defensive fixes shipped together. Issue reporter never returned with the requested diagnostic backup, but four additional users have since confirmed the same Codex breakage and ZakAnun confirmed manual cleanup is the only working workaround — defensive triple ships without the original backup grep, justified by the corroborating reports. Fix 1 (defect 3 — confirmed real). The Codex hooks emit path always appended a top-level `[[hooks]]` AoT block, which collides with users who already use the namespaced AoT form `[[hooks.SessionStart]]`. New helper `hasUserNamespacedAotHooks()` detects the user's preferred shape on parse and the install emits the GSD-managed hook in that same shape when present. Default for fresh configs stays at top-level `[[hooks]]` so status-quo behavior is preserved. Fix 2 (defects 1+2 — defensive). `stripLeakedGsdCodexSections()` (the install-time stripper) now always purges bare `[agents]` single-bracket tables and `[[agents]]` sequence tables regardless of GSD marker presence — both forms are invalid in current Codex schema and produce "invalid type: ..., expected struct AgentsToml". Previously gated on GSD-name lookup which missed marker-stripped configs and third-party authored entries. The uninstall-time stripper (`stripCodexGsdAgentSections`) keeps its old conservative behavior so user-authored entries survive uninstall. Fix 3 (defensive). Post-write schema validation parses the bytes about to be committed and asserts no bare `[agents]`, no `[[agents]]`, and no bare `[hooks.<Event>]` tables remain. On failure the install restores the pre-install backup of config.toml and aborts loudly so the user is never left with a Codex CLI that refuses to load. Pre-install snapshot is captured before installCodexConfig runs (not after) so restore returns the file to its true pre-GSD state. Tests added (10 new, 1 updated): - bug-2760-codex-install-defensive.test.cjs (10 new tests across 4 describes: hooks AoT preservation, strip robustness for both [agents] and [[agents]] without marker, schema validator behavior, abort+restore via test seam) - codex-config.test.cjs "case 2 ..." updated to reflect new defensive bare-[agents] purge Full suite: 5747 pass / 0 fail. Closes #2760 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#2760): normalize Codex hooks emit field name across migration and managed paths The migrateCodexHooksMapFormat path emitted `type = "<TYPE>"` for legacy [hooks.TYPE] sections, while the GSD-managed Codex install emitted `event = "SessionStart"` — same target [[hooks]] schema, two different field names. Codex currently tolerates both via permissive parsing, but the moment one path tightens this becomes a silent #2760-class regression. Normalize both call sites on `event` (the existing GSD-managed convention). Update migration emit, docstring, and existing migration assertions to match. Add a parity regression test that drives both code paths and asserts the [[hooks]] field key is identical. * test(#2153): fix test isolation by building hooks/dist on demand The "Codex install copies hook file (#2153)" regression depends on hooks/dist/ being populated, but that directory is gitignored and only built by `npm run build:hooks`. The npm pretest chain runs `build:sdk` but not `build:hooks`, so when this file is run in isolation (`node --test tests/codex-config.test.cjs`) the hook copy step skips silently and the regression test fails on a stale-environment artifact rather than a real bug. Add a top-level before() hook that runs scripts/build-hooks.js when hooks/dist/ is missing or empty. Matches the pattern already used by bug-1834-sh-hooks-installed and other install integration tests, so the suite passes regardless of runner ordering or which tests are targeted. * fix(#2760): structural TOML validation, atomic writes, and behavioral test rewrites Addresses CodeRabbit review on PR #2785 plus source-grep violations the maintainer flagged in the regression test. Fix 1 (CR 3149606220) — validateCodexConfigSchema now parses the TOML into a structured object first via the new parseTomlToObject helper, then runs schema-shape checks against both the parsed structure and the table section headers. Malformed TOML with valid-looking headers no longer slips past validation. Fix 2 (CR 3149606224) — Replaced the four source-grep assertions in tests/bug-2760-codex-install-defensive.test.cjs (lines 109, 125, 169, 201) with structural assertions against the parsed TOML object via the exported parseTomlToObject helper. Tests now verify behavior (the file parses and contains the expected structure) instead of literal byte patterns. Robust to formatting changes — exactly what the regex-loosening suggestion was reaching for, done correctly. Confirmed clean by `npm run lint:tests` (0 violations). Fix 3 (CR 3149606234) — The describe block that mutates installModule.__codexSchemaValidator now runs with concurrency: false so the test seam mutation cannot leak into sibling suites that also call runCodexInstall. Fix 4 (CR outside-diff) — Approach (b): atomic temp-file + renameSync. Added atomicWriteFileSync helper used by mergeCodexConfig and the final hooks-write. A mid-write failure leaves the .tmp-<pid>-<n> sibling behind (cleaned up immediately) and never truncates the original config.toml. Paired with try/catch wrapping around the entire post-snapshot mutation sequence so any unexpected throw also triggers restoreCodexSnapshot. Two layers of defense: atomic write prevents the corruption window, snapshot restore handles non-atomic write paths. Added behavioral test for fix 4: stubs fs.renameSync to throw on the configPath rename, asserts the on-disk bytes match the pre-install snapshot byte-for-byte, asserts the parsed structure is still the user's [model] section (no half-written GSD agents block), and asserts no stray .tmp-* files remain. Marked concurrency: false because it monkey-patches a global. Test results: 5749/5749 pass, 0 fail. lint:tests clean. * test(#2760): TOML-parse based assertions for bare-agents purge and hook-field parity (CodeRabbit follow-up) * fix(#2760): treat write failures as fatal, strip legacy hooks before guard, tighten TOML parser (CR4) CR4 finding 1 (MAJOR) — Write failures silently succeeded. The inner catch around atomicWriteFileSync restored the snapshot then re-threw, but the outer catch only matched 'post-write Codex schema validation failed' and downgraded everything else to a warn-and-continue. Install finished with "Done!" while Codex had no GSD agents configured. Fix: wrap writeErr with a `post-write Codex install failed:` prefix and broaden the outer guard to `.startsWith( 'post-write')` so both schema-validation and write failures abort install. CR4 finding 2 (MAJOR) — Legacy flat [[hooks]] block prevented namespaced AoT upgrade. The `!configContent.includes('gsd-check-update')` guard short- circuited the new namespaced emit when an existing install had the legacy flat [[hooks]] block, leaving users stuck in the mixed layout this fix is designed to eliminate. Fix: strip ALL existing managed gsd-check-update hook blocks (top-level [[hooks]] AND namespaced [[hooks.SessionStart]]) BEFORE evaluating the includes guard, so every install converges on the right shape regardless of prior state. CR4 finding 3 (MAJOR) — Homegrown TOML parser silently accepted malformed input. parseTomlValue happily consumed the `0` prefix of `timeout = 0.5` and parseTomlToObject did not verify the full RHS was consumed, so `key = "x" junk` and date/time literals slipped through. Per CONTRIBUTING ("No external dependencies in core"), option (b) was chosen over adding @iarna/toml: (a) parseTomlValue rejects any integer immediately followed by `.`, `e`, `E`, `:`, `-`, `T`, or `Z` (floats / dates / times); (b) parseTomlToObject scans from parsed.end to the next newline and throws `trailing bytes after value` if anything other than whitespace + optional `# comment` is present. * test(#2760): add CR4 regression tests + scope GSD_TEST_MODE + rename rename-fault test CR4 finding 5 (NIT) — GSD_TEST_MODE leak. Saved previous value, set '1' for the require, then restored (delete if undefined). No more test-only env var leaking to siblings in the same node process. CR4 finding 4 (NIT) — Renamed the existing fix-4 test from 'fs.writeFileSync' to 'fs.renameSync' (the only call actually faulted) and added a sibling test that stubs fs.writeFileSync to throw on the .tmp- target — exercising the pre-rename branch of atomicWriteFileSync that was previously untested. Both serialize via concurrency: false on the existing describe block. CR4 finding 1 (MAJOR test) — New behavioral test asserts install throws with a `post-write Codex install failed` message AND never prints "Done!" when the hook-block atomic rename fails. Captures stdout via console.log stub, asserts byte equality of restored snapshot. Faults only the rename whose temp source contains gsd-check-update so earlier mergeCodexConfig writes are not collateral damage. CR4 finding 2 (MAJOR test) — New TOML-parsed behavioral test for the legacy-hook upgrade path: pre-install has [[hooks.SessionStart]] (user) + legacy flat [[hooks]] managed gsd-check-update entry; post-install must have hooks.SessionStart as Array-of-tables with both user hook and GSD entry, and no top-level [[hooks]] AoT remaining. Also asserts exactly one gsd-check-update entry (no duplicates). CR4 finding 3 (MAJOR test) — parseTomlToObject regression suite: rejects floats (timeout = 0.5), dates (created = 1979-05-27), trailing garbage (key = "x" junk), and accepts trailing whitespace + # comment. * fix(#2760): CR5 — pre-write fatal, TOML duplicate-key/header rejection, namespaced AoT migration Address all five CodeRabbit round-5 findings on PR #2785: Finding 1 (MAJOR) — Pre-write failures in the Codex hook configuration catch (around bin/install.js:7002) used to fall through to console.warn even though restoreCodexSnapshot() had already run. This produced "Done!" output with no Codex hooks configured. Now wraps the original error with a "(pre-write)" prefix and rethrows so install aborts loudly. Same defect class as CR4 finding 1, different layer. Finding 2 (MAJOR) — parseTomlToObject silently reused existing tables and overwrote duplicate keys. Real TOML 1.0 rejects: - duplicate scalar key in same table ([a]\nx=1\nx=2) - re-declared [a] header (two [a] sections) - [[arr]] then [arr] for same path (shape mismatch) Tracks pathShape, declaredHeaders, and per-table-instance key sets; throws "duplicate or shape-mismatched table header at <path>" or "duplicate key <name> in <path>". Finding 3 (MAJOR) — migrateCodexHooksMapFormat used to emit flat [[hooks]]\nevent="<TYPE>", which produced mixed flat+namespaced layouts when the user already had [[hooks.<OTHER>]] entries. Now emits [[hooks.<TYPE>]] directly (the namespace IS the event); managed-emit detector hasUserNamespacedAotHooks fires correctly so the install converges on a single namespaced layout regardless of pre-existing state. Finding 4 (NIT) — tests/bug-2760-codex-install-defensive.test.cjs rename-failure test tightened from "throw OR warn acceptable" to assert.equal(threw, true), locking the contract Finding 1 establishes. Finding 5 (NIT) — bug-2760 test suite snapshots and restores fs.renameSync defensively in beforeEach/afterEach (symmetric with fs.writeFileSync), removing the fragile per-test try/finally. Second test in the same suite cleaned up to drop its try/finally. Updates tests/codex-config.test.cjs to assert the new namespaced AoT migration shape via parseTomlToObject (no source-grep). Existing field- parity test reframed as shape-parity since both paths now emit namespaced. Tests: 5764 pass (+8 new). lint:tests: 0 violations. * docs(#2760): add CHANGELOG entry for Codex install defensive triple Adds the [Unreleased] Fixed entry for the Codex install fix landed in this PR — defensive strip of legacy [agents]/[[agents]] blocks, namespaced AoT hook detection across all events, atomic write + rollback, strict TOML validation rejecting duplicate keys/repeated headers/trailing bytes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
6a293cfc2a |
fix(#2775): verify gsd-sdk on PATH before reporting SDK ready (#2777)
* fix(#2775): verify gsd-sdk on PATH before reporting SDK ready `npx get-shit-done-cc@latest` printed `✓ GSD SDK ready` even though `gsd-sdk` was not callable. Root cause: npx only links the package's primary bin (`get-shit-done-cc`); secondary bins like `gsd-sdk` are not materialized into a PATH directory. The installer asserted the weaker invariant "sdk/dist/cli.js exists on disk" and treated it as proof of the stronger invariant "command -v gsd-sdk resolves" — they aren't the same. Fix tightens the gate in installSdkIfNeeded: 1. After confirming the dist is present, walk PATH for an executable `gsd-sdk` shim (isGsdSdkOnPath, no spawn). 2. If absent, attempt to materialize the shim via symlink at `~/.local/bin/gsd-sdk` (or the first HOME-rooted PATH dir we can write to), falling back to a copy on filesystems that reject symlinks (trySelfLinkGsdSdk). 3. Re-probe PATH after linking. Only print `✓ GSD SDK ready` when the probe succeeds; otherwise emit a clear ⚠ + remediation. Also strips the misleading "or `npx get-shit-done-cc`" clause from the shim header (it never linked the secondary bin). Closes #2775 * test(#2775): use centralized helpers from helpers.cjs per CONTRIBUTING * fix(#2775): wrapper script in symlink fallback to preserve __dirname resolution CodeRabbit follow-up on PR #2777. The previous symlink-fallback in trySelfLinkGsdSdk used fs.copyFileSync(shimSrc, target), but bin/gsd-sdk.js resolves the CLI via path.resolve(__dirname, '..', 'sdk', 'dist', 'cli.js'). After a copy, __dirname becomes the link directory (e.g. ~/.local/bin), so the resolved CLI path was broken (~/.local/sdk/dist/cli.js) — and isGsdSdkOnPath() only checked file existence + execute bit, so the success line still printed over a broken install. Replace the copy with a tiny wrapper script that require()s the real shim by absolute path. This preserves __dirname inside bin/gsd-sdk.js because the require runs against shimSrc's own location. Also fixes the PATH restoration nit in the regression test (was coercing undefined to the string "undefined" if PATH was unset). Adds a behavioral fallback test that mocks fs.symlinkSync to throw, exercises the fallback path, and asserts the resulting target is a require()-wrapper (not a verbatim copy) and is executable. * fix(#2775): PATH-backed dir ordering + tighten captureConsole + drop tautological assertion (CodeRabbit follow-up) |
||
|
|
290c8b2909 |
fix(#2771): unify user-owned-artifacts list to suppress false patches warning (#2776)
* fix(#2771): unify user-owned-artifacts list to suppress false patches warning USER-PROFILE.md was both preserved across reinstalls (correctly) AND tracked in gsd-file-manifest.json (incorrectly). On the next install, saveLocalPatches() hashed the on-disk file, found it differed from the stale manifest hash (because /gsd-profile-user --refresh regenerated it), and reported it as a "locally modified GSD file" — a spurious warning every time the profile refreshed. A file is either distribution (manifest-tracked, diff'd against manifest) or user artifact (preserved across installs, never diff'd). Never both. This extracts USER_OWNED_ARTIFACTS as a single source of truth, referenced by both the preserveUserArtifacts call site and writeManifest, so the invariant cannot drift again. Adds a regression test that exercises the full reproduction path: install, create USER-PROFILE.md, reinstall, refresh USER-PROFILE.md, reinstall, assert no patch backup and no warning text. Closes #2771 * test(#2771): use centralized helpers from helpers.cjs per CONTRIBUTING * fix(#2771): normalize legacy USER_OWNED_ARTIFACTS entries from manifest + tighten test |
||
|
|
9472f343db |
feat(#2762): --minimal install profile (≥94% cold-start token reduction) (#2764)
* feat(#2762): add --minimal install profile to cut cold-start token cost Eager system-prompt load from 86 gsd-* skill descriptions plus 33 subagent descriptions costs ~12k tokens per turn even in directories with no .planning/. Frontier models (Sonnet 4.6 / Opus 4.7) with 200K-1M context don't feel it; local LLMs with 32K-128K do. --minimal (alias --core-only) installs only the main GSD loop: new-project, discuss-phase, plan-phase, execute-phase, plus help/update. Zero gsd-* subagents are written. Re-running gsd update without --minimal expands to the full surface. Default install behavior is unchanged. DRY: a single stageSkillsForMode() helper filters the source dir; all 13 runtime-specific copy fns are unchanged because they recurse the staged dir. Allowlist + helpers live in get-shit-done/bin/lib/install- profiles.cjs as the single source of truth. Manifest now records mode: 'minimal' | 'full' so future commands can detect install profile. Tested end-to-end: --minimal yields 6 skill folders + 0 agents; default yields 86 + 33 (unchanged). * docs(#2762): document --minimal install in README Adds a collapsible 'Minimal Install' section under Getting Started covering: who it's for (local LLMs, token-billed APIs), what you get (6 skills, 0 subagents, ~700 token floor vs ~12k), and the critical caveat that re-installing without --minimal restores the full surface and erases the savings. Includes a comparison table, the manifest inspection one-liner, and the use-case decision matrix. * fix(#2762): address CodeRabbit review + CI failures CodeRabbit findings: 1. Temp dir leak (Minor): stageSkillsForMode created tmp dirs that were never cleaned up. Added a module-level Set tracking every staged dir plus a process.on('exit') handler that rm -rf's them. Also wrap the copy loop in try/catch to remove a partially-populated tmp dir on mid-flight failure. Verified end-to-end: 0 leaked dirs in /tmp after a real install. 2. Codex full -> minimal stale state (Major): a previous full Codex install left agents/gsd-*.toml files plus [agents.gsd-*] sections in config.toml. The original cleanup only removed .md files, so a switch to --minimal would leave Codex still advertising the full agent surface. Cleanup now also handles .toml under isCodex, and minimal mode strips GSD sections from config.toml via the existing stripGsdFromCodexConfig helper (same path used by --uninstall). 3. Nitpick — Codex downgrade regression test: added a spawnSync-based end-to-end test that fakes a previous full install (stale gsd-*.md + gsd-*.toml + GSD-marked config.toml + a user-owned agent/setting), runs install.js --codex --minimal, and asserts stale GSD files + sections are gone while user content is preserved. CI failures (inventory parity): - docs/INVENTORY.md CLI Modules table now lists install-profiles.cjs with the correct headline count (30 -> 31). - docs/INVENTORY-MANIFEST.json regenerated via gen-inventory-manifest.cjs. Test count: 149 pass (was 116 in last commit; +14 new install-minimal + all previously-failing inventory tests now green). * test(#2762): expand install-minimal test coverage for future-proofing Each new test pins a specific guarantee that closes off a future regression class — turning every CodeRabbit finding (including the nitpicky one) into a permanent guard. cleanupStagedSkills suite (+3 tests): - 'full mode does not register a staged dir' — catches a future regression where someone forgets the early-return in stageSkillsForMode and starts polluting STAGED_DIRS in default installs. - 'exit handler registers exactly once across many calls' — catches removal of the exitHandlerRegistered guard. install.js has 13 dispatch sites, so a missing guard would attach 13 listeners. - 'mid-copy failure removes partial staged dir and re-throws' — intercepts fs.copyFileSync to throw mid-loop and asserts the staged dir count in /tmp is unchanged after the throw. Pins the exact CodeRabbit-flagged leak. Claude full -> minimal downgrade (+1 test): - Mirrors the Codex downgrade test for the .md-only path that the other 12 runtimes share. Asserts user-owned agents are preserved. Manifest mode round-trip (+3 tests): - Default install -> mode: 'full' with >6 skills and >0 agents - --minimal -> mode: 'minimal' with exactly 6 skills and 0 agents - --core-only alias produces identical manifest to --minimal Allowlist scope guards (+3 tests): - Every main-loop command IS in allowlist (positive) - Off-loop commands (autonomous, ship, do, progress, next, fast, quick, debug, code-review, verify-work) are NOT (guards against silent scope creep — future contributor adds 'autonomous' to core and the floor erodes) - Unknown mode strings fall through to full behavior — pre-emptive guard for future 'compact'/'tier2' modes that might forget to update the predicate. Total: 25 tests in this file (was 15), 159/159 passing across the install + inventory suites. * fix(#2762): clean up staged tmp dirs on SIGINT/SIGTERM/SIGHUP CodeRabbit follow-up review on c727bf5f flagged that process.on('exit') does not fire on signal-driven termination. An installer is exactly the kind of process users abort mid-run with Ctrl+C, so without explicit signal handlers the staged tmp dirs in STAGED_DIRS would be left behind until the OS reaps tmpdir. Fix: ensureExitCleanup now also registers process.once handlers for SIGINT, SIGTERM, SIGHUP. Each handler runs cleanupStagedSkills then re-raises the same signal via process.kill(pid, sig) so the OS-default handler takes over and the parent shell sees the correct exit code (130 for SIGINT, etc.) — CI scripts and interactive users see the abort the way they expect. Test: spawns a child that stages a tmp dir then blocks; parent captures the staged path from stdout, sends SIGINT, asserts (a) the staged dir is gone after child exit, (b) child exits via the signal not via code 0. Skipped on Windows (signal semantics differ; the natural-exit cleanup test covers the Windows CI matrix). Total: 26 tests in install-minimal.test.cjs (was 25). |
||
|
|
d5cd64dde5 |
fix(#2637): migrate legacy Codex [hooks] map format to [[hooks]] array on install (#2747)
Codex 0.124.0 changed the required config.toml hooks format from the old map-style ([hooks.shell]) to array-of-tables ([[hooks]]). Old GSD installs that wrote the legacy format now cause a startup parse error on upgrade. Add migrateCodexHooksMapFormat() which detects non-array [hooks] and [hooks.TYPE] sections and rewrites them to [[hooks]] entries with an injected type = "TYPE" key. The migration runs at the start of every Codex install so affected configs self-heal on the next `gsd install --codex`. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
7924abec0c |
fix(installer): revert Codex agents section to [agents.<name>] struct format
[[agents]] sequence format (introduced in #2645) is rejected by codex-cli 0.124.0 with "invalid type: sequence, expected struct AgentsToml". Revert to [agents.<name>] struct format which is correct for 0.120.0+. stripCodexGsdAgentSections already handles both formats for self-healing configs written by previous GSD versions using [[agents]]. Closes #2727 |
||
|
|
3da9420a38 |
fix(#2698,#2678): CRLF agent-block strip regex + local install skips SDK check (#2710)
Fixes #2698 — The two separate LF/CRLF .replace() calls in the Codex hooks migration could not handle mixed line endings (e.g. header in LF, body in CRLF), leaving stale gsd-update-check blocks after reinstall. Consolidated to a single \r?\n-aware regex with gm flags that handles LF, CRLF, and mixed content in one pass. Fixes #2678 — installSdkIfNeeded() called process.exit(1) unconditionally when sdk/dist/cli.js was missing, even during --local installs where users cannot write to global node_modules. Added isLocal option: when true, prints a warning and returns instead of exiting. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
7ed05c8811 |
fix(#2645): emit [[agents]] array-of-tables in Codex config.toml (#2664)
* fix(#2645): emit [[agents]] array-of-tables in Codex config.toml Codex ≥0.116 rejects `[agents.<name>]` map tables with `invalid type: map, expected a sequence`. Switch generateCodexConfigBlock to emit `[[agents]]` array-of-tables with an explicit `name` field per entry. Strip + merge paths now self-heal on reinstall — both the legacy `[agents.gsd-*]` map shape (pre-#2645 configs) and the new `[[agents]]` with `name = "gsd-*"` shape are recognized and replaced, while user-authored `[[agents]]` entries are preserved. Fixes #2645 * fix(#2645): use TOML-aware parser to strip managed [[agents]] sections CodeRabbit flagged that the prior regex-based stripper for [[agents]] array-of-tables only matched headers at column 0 and stopped at any line beginning with `[`. An indented [[agents]] header would not terminate the preceding match, so a managed `gsd-*` block could absorb a following user-authored agent and silently delete it. Replace the ad-hoc regex with the existing TOML-aware section parser (getTomlTableSections + removeContentRanges) so section boundaries are authoritative regardless of indentation. Same logic applies to legacy [agents.gsd-*] map sections. Add a comprehensive mixed-shape test covering multiple GSD entries (both legacy map and new array-of-tables, double- and single-quoted names) interleaved with multiple user-authored agents in both shapes — verifies all GSD entries are stripped and every user entry is preserved. |
||
|
|
709f0382bf |
fix(#2639): route Codex TOML emit through full Claude→Codex neutralization pipeline (#2657)
installCodexConfig() applied a narrow path-only regex pass before generateCodexAgentToml(), skipping the convertClaudeToCodexMarkdown() + neutralizeAgentReferences(..., 'AGENTS.md') pipeline used on the .md emit path. Result: emitted Codex agent TOMLs carried stale Claude-specific references (CLAUDE.md, .claude/skills/, .claude/commands/, .claude/agents/, .claudeignore, bare "Claude" agent-name mentions). Route the TOML path through convertClaudeToCodexMarkdown and extend that pipeline to cover bare .claude/<subdir>/ references and .claudeignore (both previously unhandled on the .md path too). The $HOME/.claude/ get-shit-done prefix substitution still runs first so the absolute Codex install path is preserved before the generic .claude → .codex rewrite. Regression test: tests/issue-2639-codex-toml-neutralization.test.cjs — drives installCodexConfig against a fixture containing every flagged marker and asserts the emitted TOML contains zero CLAUDE.md / .claude/ / .claudeignore occurrences and that Claude Code / Claude Opus product names survive. Fixes #2639 |
||
|
|
b67ab38098 |
fix(#2643): align skill frontmatter name with workflow gsd: emission (#2672)
Flat-skills installs write SKILL.md files under gsd-<cmd>/ dirs, but Claude Code resolves skills by their frontmatter `name:`, not directory name. PR #2595 normalized every `/gsd-<cmd>` to `/gsd:<cmd>` across workflows — including inside `Skill(skill="...")` args — but the installer still emitted `name: gsd-<cmd>`, so every Skill() call on a flat-skills install resolved to nothing. Fix: emit `name: gsd:<cmd>` (colon form) in `convertClaudeCommandToClaudeSkill`. Keep the hyphen-form directory name for Windows path safety. Codex stays on hyphen form: its adapter invokes skills as `$gsd-<cmd>` (shell-var syntax) and a colon would terminate the variable name. `convertClaudeCommandToCodexSkill` uses `yamlQuote(skillName)` directly and is untouched. - Extract `skillFrontmatterName(dirName)` helper (exported for tests). - Update claude-skills-migration and qwen-skills-migration assertions that encoded the old hyphen emission. - Add `tests/bug-2643-skill-frontmatter-name.test.cjs` asserting every `Skill(skill="gsd:<cmd>")` reference in workflows resolves to an emitted frontmatter name. Full suite: 5452/5452 passing. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
8caa7d4c3a |
fix(#2649): installer fail-fast when sdk/dist missing in npx cache (#2667)
Root cause shared with #2647: a broken 1.38.3 tarball shipped without sdk/dist/. The pre-#2441-decouple installer reacted by running spawnSync('npm.cmd', ['install'], { cwd: sdkDir }) inside the npx cache on Windows, where the cache is read-only, producing the misleading "Failed to npm install in sdk/" error. Defensive changes here (user-facing behavior only; packaging fix lives in the sibling PR for #2647): - Classify the install context (classifySdkInstall): detect npx cache paths, node_modules-based installs, and dev clones via path heuristics plus a side-effect-free write probe. Exported for test. - Rewrite the dist-missing error to branch on context: tarball + npxCache -> "don't touch npx cache; npm i -g ...@latest" tarball (other) -> upgrade path + clone-build escape hatch dev-clone -> keep existing cd sdk && npm install && npm run build - Preserve the invariant that the installer never shells out to npm install itself — users always drive that. - Add tests/bug-2649-sdk-fail-fast.test.cjs covering the classifier and both failure messages, with spawnSync/execSync interceptors that assert no nested npm install is attempted. Cross-ref: #2647 (packaging). Fixes #2649 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5a8a6fb511 |
fix(#2256): pass per-agent model overrides through Codex/OpenCode transport (#2628)
The Codex and OpenCode install paths read `model_overrides` only from `~/.gsd/defaults.json` (global). A per-project override set in `.planning/config.json` — the reporter's exact setup for `gsd-codebase-mapper` — was silently dropped, so the child agent inherited the runtime's default model regardless of `model_overrides`. Neither runtime has an inline `model` parameter on its spawn API (Codex `spawn_agent(agent_type, message)`, OpenCode `task(description, prompt, subagent_type, task_id, command)`), so the per-agent model must reach the child via the static config GSD writes at install time. That config was being populated from the wrong source. Fix: add `readGsdEffectiveModelOverrides(targetDir)` which merges `~/.gsd/defaults.json` with per-project `.planning/config.json`, with per-project keys winning on conflict. Both install sites now call it and walk up from the install root to locate `.planning/` — matching the precedence `readGsdRuntimeProfileResolver` already uses for #2517. Also update the Codex Task()->spawn_agent mapping block so it no longer says "omit" without context: it now documents that per-agent overrides are embedded in the agent TOML and notes the restriction that Codex only permits `spawn_agent` when the user explicitly requested sub-agents (do the work inline otherwise). Regression tests (`tests/bug-2256-model-overrides-transport.test.cjs`) cover: global-only, project-only, project-wins-on-conflict, walking up from a nested `targetDir`, Codex TOML `model =` emission, and OpenCode frontmatter `model:` emission. Closes #2256 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
807db75d55 |
fix(#2620): detect HOME-relative PATH entries before suggesting absolute export (#2625)
* fix(#2620): detect HOME-relative PATH entries before suggesting absolute export When the installer reported `gsd-sdk` not on PATH and suggested appending an absolute `export PATH="/home/user/.npm-global/bin:$PATH"` line to the user's rc file, a user who had the equivalent `export PATH="$HOME/.npm-global/bin:$PATH"` already in their shell profile would get a duplicate entry — the installer only compared the absolute form. Add `homePathCoveredByRc(globalBin, homeDir, rcFileNames?)` to `bin/install.js` and export it for test-mode callers. The helper scans `~/.zshrc`, `~/.bashrc`, `~/.bash_profile`, `~/.profile`, grepping each file for `export PATH=` / bare `PATH=` lines and substituting the common HOME forms (\$HOME, \${HOME}, leading ~/) with the real home directory before comparing each resolved PATH segment against globalBin. Trailing slashes are normalised so `.npm-global/bin/` matches `.npm-global/bin`. Missing / unreadable / malformed rc files are swallowed — the caller falls back to the existing absolute suggestion. Tests cover $HOME, \${HOME}, and ~/ forms, absolute match, trailing-slash match, commented-out lines, missing rc files, and unreadable rc files (directory where a file is expected). Closes #2620 * fix(#2620): skip relative PATH segments in homePathCoveredByRc CodeRabbit flagged that the helper unconditionally resolved every non-$-containing segment against homeAbs via path.resolve(homeAbs, …), which silently turns a bare relative segment like `bin` or `node_modules/.bin` into `$HOME/bin` / `$HOME/node_modules/.bin`. That is wrong: bare PATH segments depend on the shell's cwd at lookup time, not on $HOME — so the helper was returning true for rc files that do not actually cover globalBin. Guard the compare with path.isAbsolute(expanded) after HOME expansion. Only segments that are absolute on their own (or that became absolute via $HOME / \${HOME} / ~ substitution) are compared against targetAbs. Relative segments are skipped. Add two regression tests covering a bare `bin` segment and a nested `node_modules/.bin` segment; both previously returned true when home happened to contain a matching subdirectory and now correctly return false. Closes #2620 (CodeRabbit follow-up) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#2620): wire homePathCoveredByRc into installer suggestion path CodeRabbit flagged that homePathCoveredByRc was added in the previous commit but never called from the installer, so the user-facing PATH warning stayed unchanged — users with `export PATH="$HOME/.npm-global/bin:$PATH"` in their rc would still get a duplicate absolute-path suggestion. Add `maybeSuggestPathExport(globalBin, homeDir)` that: - skips silently when globalBin is already on process.env.PATH; - prints a "try reopening your shell" diagnostic when homePathCoveredByRc returns true (the directory IS on PATH via an rc entry — just not in the current shell); - otherwise falls through to the absolute-path `echo 'export PATH="…:$PATH"' >> ~/.zshrc` suggestion. Call it from installSdkIfNeeded after the sdk/dist check succeeds, resolving globalBin via `npm prefix -g` (plus `/bin` on POSIX). Swallow any exec failure so the installer keeps working when npm is weird. Export maybeSuggestPathExport for tests. Add three new regression tests (installer-flow coverage per CodeRabbit nitpick): - rc covers globalBin via $HOME form → no absolute suggestion emitted - rc covers only an unrelated directory → absolute suggestion emitted - globalBin already on process.env.PATH → no output at all Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0a049149e1 |
fix(sdk): decouple from build-from-source install, close #2441 #2453 (#2457)
* fix(sdk): decouple SDK from build-from-source install path, close #2441 and #2453 Ship sdk/dist prebuilt in the tarball and replace the npm-install-g sub-install with a parent-package bin shim (bin/gsd-sdk.js). npm chmods bin entries from a packed tarball correctly, eliminating the mode-644 failure (#2453) and the full class of NPM_CONFIG_PREFIX/ignore-scripts/ corepack/air-gapped failure modes that caused #2439 and #2441. Changes: - sdk/package.json: prepublishOnly runs `rm -rf dist && tsc && chmod +x dist/cli.js` (stale-build guard + execute-bit fix at publish time) - package.json: add "gsd-sdk": "bin/gsd-sdk.js" bin entry; add sdk/dist to files so the prebuilt CLI ships in the tarball - bin/gsd-sdk.js: new back-compat shim — resolves sdk/dist/cli.js relative to the package root and delegates via `node`, so all existing PATH call sites (slash commands, agents, hooks) continue to work unchanged (S1 shim) - bin/install.js: replace installSdkIfNeeded() build-from-source + global- install dance with a dist-verify + chmod-in-place guard; delete resolveGsdSdk(), detectShellRc(), emitSdkFatal() helpers now unused - .github/workflows/install-smoke.yml: add smoke-unpacked job that strips execute bit from sdk/dist/cli.js before install to reproduce the exact #2453 failure mode - tests/bug-2441-sdk-decouple.test.cjs: new regression tests asserting all invariants (no npm install -g from sdk/, shim exists, sdk/dist in files, prepublishOnly has rm -rf + chmod) - tests/bugs-1656-1657.test.cjs: update stale assertions that required build-from-source behavior (now asserts new prebuilt-dist invariants) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(release): bump to 1.38.2, wire release.yml to build SDK dist - Bump version 1.38.1 -> 1.38.2 for the #2441/#2453 fix shipped in 0f6903d. - Add `build:sdk` script (`cd sdk && npm ci && npm run build`). - `prepublishOnly` now runs hooks + SDK builds as a safety net. - release.yml (rc + finalize): build SDK dist before `npm publish` so the published tarball always ships fresh `sdk/dist/` (kept gitignored). - CHANGELOG: document 1.38.2 entry and `--sdk` flag semantics change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: build SDK dist before tests and smoke jobs sdk/dist/ is gitignored (built fresh at publish time via release.yml), but both the test suite and install-smoke jobs run `bin/install.js` or `npm pack` against the checked-out tree where dist doesn't exist yet. - test.yml: `npm run build:sdk` before `npm run test:coverage`, so tests that spawn `bin/install.js` don't hit `installSdkIfNeeded()`'s fatal missing-dist check. - install-smoke.yml (both smoke and smoke-unpacked): build SDK before pack/chmod so the published tarball contains dist and the unpacked install has a file to strip exec-bit from. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sdk): lift SDK runtime deps to parent so tarball install can resolve them The SDK's runtime deps (ws, @anthropic-ai/claude-agent-sdk) live in sdk/package.json, but sdk/node_modules is NOT shipped in the parent tarball — only sdk/dist, sdk/src, sdk/prompts, and sdk/package.json are. When a user runs `npm install -g get-shit-done-cc`, npm installs the parent's node_modules but never runs `npm install` inside the nested sdk/ directory. Result: `node sdk/dist/cli.js` fails with ERR_MODULE_NOT_FOUND for 'ws'. The smoke tarball job caught this; the unpacked variant masked it because `npm install -g <dir>` copies the entire workspace including sdk/node_modules (left over from `npm run build:sdk`). Fix: declare the same deps in the parent package.json so they land in <pkg>/node_modules, which Node's resolution walks up to from <pkg>/sdk/dist/cli.js. Keep them declared in sdk/package.json too so the SDK remains a self-contained package for standalone dev. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lockfile): regenerate package-lock.json cleanly The previous `npm install` run left the lockfile internally inconsistent (resolved esbuild@0.27.7 referenced but not fully written), causing `npm ci` to fail in CI with "Missing from lock file" errors. Clean regen via rm + npm install fixes all three failed jobs (test, smoke, smoke-unpacked), which were all hitting the same `npm ci` sync check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(deps): remove unused esbuild + vitest from root devDependencies Both were declared but never imported anywhere in the root package (confirmed via grep of bin/, scripts/, tests/). They lived in sdk/ already, which is the only place they're actually used. The transitive tree they pulled in (vitest → vite → esbuild 0.28 → @esbuild/openharmony-arm64) was the root of the CI npm ci failures: the openharmony platform package's `optional: true` flag was not being applied correctly by npm 10 on Linux runners, causing EBADPLATFORM. After removal: 800+ transitive packages → 155. Lockfile regenerated cleanly. All 4170 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sdk): pretest:coverage builds sdk; tighten shim test assertions Add "pretest:coverage": "npm run build:sdk" so npm run test:coverage works in clean checkouts where sdk/dist/ hasn't been built yet. Tighten the two loose shim assertions in bug-2441-sdk-decouple.test.cjs: - forwards-to test now asserts path.resolve() is called with the 'sdk','dist','cli.js' path segments, not just substring presence - node-invocation test now asserts spawnSync(process.execPath, [...]) pattern, ruling out matches in comments or the shebang line Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address PR review — pretest:coverage + tighten shim tests Review feedback from trek-e on PR 2457: 1. pretest:coverage + pretest hooks now run `npm run build:sdk` so `npm run test[:coverage]` in a clean checkout produces the required sdk/dist/ artifacts before running the installer-dependent tests. CI already does this explicitly; local contributors benefit. 2. Shim tests in bug-2441-sdk-decouple.test.cjs tightened from loose substring matches (which would pass on comments/shebangs alone) to regex assertions on the actual path.resolve call, spawnSync with process.execPath, process.argv.slice(2), and process.exit pattern. These now provide real regression protection for #2453-class bugs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: correct CHANGELOG entry and add [1.38.2] reference link Two issues in the 1.38.2 CHANGELOG entry: - installSdkIfNeeded() was described as deleted but it still exists in bin/install.js (repurposed to verify sdk/dist/cli.js and fix execute bit). Corrected the description to say 'repurposes' rather than 'deletes'. - The reference-link block at the bottom of the file was missing a [1.38.2] compare URL and [Unreleased] still pointed to v1.37.1...HEAD. Added the [1.38.2] link and updated [Unreleased] to compare/v1.38.2...HEAD. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): double-cast WorkflowConfig to Record for strict tsc build TypeScript error on main (introduced in #2611) blocks `npm run build` in sdk/, which now runs as part of this PR's tarball build path. Apply the double-cast via `unknown` as the compiler suggests. Same fix as #2622; can be dropped if that lands first. * test: remove bug-2598 test obsoleted by SDK decoupling The bug-2598 test guards the Windows CVE-2024-27980 fix in the old build-from-source path (npm spawnSync with shell:true + formatSpawnFailure diagnostics). This PR removes that entire code path — installSdkIfNeeded no longer spawns npm, it just verifies the prebuilt sdk/dist/cli.js shipped in the tarball. The test asserts `installSdkIfNeeded.toString()` contains a formatSpawnFailure helper. After decoupling, no such helper exists (nothing to format — there's no spawn). Keeping the test would assert invariants of the rejected architecture. The original #2598 defect (silent failure of npm spawn on Windows) is structurally impossible in the shim path: bin/gsd-sdk.js invokes `node sdk/dist/cli.js` directly via child_process.spawn with an explicit argv array. No .cmd wrapper, no shell delegation. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
cc17886c51 |
feat: make model profiles runtime-aware for Codex/non-Claude runtimes (closes #2517) (#2609)
* feat: make model profiles runtime-aware for Codex/non-Claude runtimes (closes #2517) Adds an optional top-level `runtime` config key plus a `model_profile_overrides[runtime][tier]` map. When `runtime` is set, profile tiers (opus/sonnet/haiku) resolve to runtime-native model IDs (and reasoning_effort where supported) instead of bare Claude aliases. Codex defaults from the spec: opus -> gpt-5.4 reasoning_effort: xhigh sonnet -> gpt-5.3-codex reasoning_effort: medium haiku -> gpt-5.4-mini reasoning_effort: medium Claude defaults mirror MODEL_ALIAS_MAP. Unknown runtimes fall back to the Claude-alias safe default rather than emit IDs the runtime cannot accept. reasoning_effort is only emitted into Codex install paths; never returned from resolveModelInternal and never written to Claude agent frontmatter. Backwards compatible: any user without `runtime` set sees identical behavior — the new branch is gated on `config.runtime != null`. Precedence (highest to lowest): 1. per-agent model_overrides 2. runtime-aware tier resolution (when `runtime` is set) 3. resolve_model_ids: "omit" 4. Claude-native default 5. inherit (literal passthrough) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(#2517): address adversarial review of #2609 (findings 1-16) Addresses all 16 findings from the adversarial review of PR #2609. Each finding is enumerated below with its resolution. CRITICAL - F1: readGsdRuntimeProfileResolver(targetDir) now probes per-project .planning/config.json AND ~/.gsd/defaults.json with per-project winning, so the PR's headline claim ("set runtime in project config and Codex TOML emit picks it up") actually holds end-to-end. - F2: resolveTierEntry field-merges user overrides with built-in defaults. The CONFIGURATION.md string-shorthand example `{ codex: { opus: "gpt-5-pro" } }` now keeps reasoning_effort from the built-in entry. Partial-object overrides like `{ opus: { reasoning_effort: 'low' } }` keep the built-in model. Both paths regression-tested. MAJOR - F3: resolveReasoningEffortInternal gates strictly on the RUNTIMES_WITH_REASONING_EFFORT allowlist regardless of override presence. Override + unknown-runtime no longer leaks reasoning_effort. - F4: runtime:"claude" is now a no-op for resolution (it is the implicit default). It no longer hijacks resolve_model_ids:"omit". Existing tests for `runtime:"claude"` returning Claude IDs were rewritten to reflect the no-op semantics; new test asserts the omit case returns "". - F5: _readGsdConfigFile in install.js writes a stderr warning on JSON parse failure instead of silently returning null. Read failure and parse failure are warned separately. Library require is hoisted to top of install.js so it is not co-mingled with config-read failure modes. - F6: install.js requires for core.cjs / model-profiles.cjs are hoisted to the top of the file with __dirname-based absolute paths so global npm install works regardless of cwd. Test asserts both lib paths exist relative to install.js __dirname. - F7: docs/CONFIGURATION.md `runtime` row no longer lists `opencode` as a valid runtime — install-path emission for non-Codex runtimes is explicitly out of scope per #2517 / #2612, and the doc now points at #2612 for the follow-on work. resolveModelInternal still accepts any runtime string (back-compat) and falls back safely for unknown values. - F8: Tests now isolate HOME (and GSD_HOME) to a per-test tmpdir so the developer's real ~/.gsd/defaults.json cannot bleed into assertions. Same pattern CodeRabbit caught on PRs #2603 / #2604. - F9: `runtime` and `model_profile_overrides` documented as flat-only in core.cjs comments — not routed through `get()` because they are top-level keys per docs/CONFIGURATION.md and introducing nested resolution for two new keys was not worth the edge-case surface. - F10/F13: loadConfig now invokes _warnUnknownProfileOverrides on the raw parsed config so direct .planning/config.json edits surface unknown runtime values (e.g. typo `runtime: "codx"`) and unknown tier values (e.g. `model_profile_overrides.codex.banana`) at read time. Warnings only — preserves back-compat for runtimes added later. Per-process warning cache prevents log spam across repeated loadConfig calls. MINOR / NIT - F11: Removed dead `tier || 'sonnet'` defensive shortcut. The local is now `const alias = tier;` with a comment explaining why `tier` is guaranteed truthy at that point (every MODEL_PROFILES entry defines `balanced`, the fallback profile). - F12: Extracted resolveTierEntry() in core.cjs as the single source of truth for runtime-aware tier resolution. core.cjs and bin/install.js both consume it — no duplicated lookup logic between the two files. - F14: Added regression tests for findings #1, #2, #3, #4, #6, #10, #13 in tests/issue-2517-runtime-aware-profiles.test.cjs. Each must-fix path has a corresponding test that fails against the pre-fix code and passes against the post-fix code. - F15: docs/CONFIGURATION.md `model_profile` row cross-references #1713 / #1806 next to the `adaptive` enum value. - F16: RUNTIME_PROFILE_MAP remains in core.cjs as the single source of truth; install.js imports it through the exported resolveTierEntry helper rather than carrying its own copy. Doc files (CONFIGURATION.md, USER-GUIDE.md, settings.md) intentionally still embed the IDs as text — code comment in core.cjs flags that those doc files must be updated whenever the constant changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c90081176d |
fix(#2598): pass shell: true to npm spawnSync on Windows (#2600)
* fix(#2598): pass shell: true to npm spawnSync on Windows Since Node's CVE-2024-27980 fix (>= 18.20.2 / >= 20.12.2 / >= 21.7.3), spawnSync refuses to launch .cmd/.bat files on Windows without `shell: true`. installSdkIfNeeded picks npmCmd='npm.cmd' on win32 and then calls spawnSync five times — every one returns { status: null, error: EINVAL } before npm ever runs. The installer checks `status !== 0`, trips the failure path, and emits a bare "Failed to `npm install` in sdk/." with zero diagnostic output because `stdio: 'inherit'` never had a child to stream. Every fresh install on Windows has failed at the SDK build step on any supported Node version for the life of the post-CVE bin/install.js. Introduce a local `spawnNpm(args, opts)` helper inside installSdkIfNeeded that injects `shell: process.platform === 'win32'` when the caller doesn't override it. Route all five npm invocations through it: `npm install`, `npm run build`, `npm install -g .`, and both `npm config get prefix` calls. Adds a static regression test that parses installSdkIfNeeded and asserts no bare `spawnSync(npmCmd, ...)` remains, a shell-aware wrapper exists, and at least five invocations go through it. Closes #2598 * fix(#2598): surface spawnSync diagnostics in SDK install fatal paths Thread result.error / result.signal / result.status into emitSdkFatal for the three npm failure branches (install, run build, install -g .) via a formatSpawnFailure helper. The root cause of #2598 went silent precisely because `{ status: null, error: EINVAL }` was reduced to a generic "Failed to `npm install` in sdk/." with no diagnostic — stdio: 'inherit' had no child process to stream and result.error was swallowed. Any future regression in the same area (EINVAL, ENOENT, signal termination) now prints its real cause in the red fatal banner. Also strengthen the regression test so it cannot pass with only four real npm call sites: the previous `spawnSync(npmCmd, ..., shell)` regex double-counted the spawnNpm helper's own body when a helper existed. Separate arrow-form vs function-form helper detection and exclude the wrapper body from explicitShellNpm so the `>= 5` assertion reflects real invocations only. Add a new test that asserts all three fatal branches now reference formatSpawnFailure / result.error / signal / status. Addresses CodeRabbit review comments on PR #2600: - r3126987409 (bin/install.js): surface underlying spawnSync failure - r3126987419 (test): explicitShellNpm overcounts by one via helper def |
||
|
|
349daf7e6a |
fix(#2545): use word boundary in path replacement to catch ~/.claude without trailing slash (#2592)
The Copilot content converter only replaced `~/.claude/` and `$HOME/.claude/` when followed by a literal `/`. Bare references (e.g. `configDir = ~/.claude` at end of line) slipped through and triggered the post-install "Found N unreplaced .claude path reference(s)" warning, since the leak scanner uses `(?:~|$HOME)/\.claude\b`. Switched both replacements to a `(\/|\b)` capture group so trailing-slash and bare forms are handled in a single pass — matching the pattern already used by Antigravity, OpenCode, Kilo, and Codex converters. Closes #2545 |
||
|
|
fba040c72c |
fix(#2557): Gemini/Antigravity local hook commands use relative paths, not \$CLAUDE_PROJECT_DIR (#2589)
\$CLAUDE_PROJECT_DIR is Claude Code-specific. Gemini CLI doesn't set it, and on Windows its path-join logic doubled the value producing unresolvable paths like D:\Projects\GSD\'D:\Projects\GSD'. Gemini runs project hooks with project root as cwd, so bare relative paths (e.g. node .gemini/hooks/gsd-check-update.js) are cross-platform and correct. Claude Code and others still use the env var. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
8eeaa20791 |
fix(install): chmod dist/cli.js 0o755 after npm install -g; add regression test (closes #2525) (#2536)
Use process.platform !== 'win32' guard in catch instead of a comment, and add regression test for bug #2525 (gsd-sdk bin symlink points at non-executable file). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
280eed93bc |
feat(cli): add /gsd-sync-skills for cross-runtime managed skill sync (#2491)
* fix(tests): update 5 source-text tests to read config-schema.cjs VALID_CONFIG_KEYS moved from config.cjs to config-schema.cjs in the drift-prevention companion PR. Tests that read config.cjs source text and checked for key literal includes() now point to the correct file. Closes #2480 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(cli): add /gsd-sync-skills for cross-runtime managed skill sync (#2380) Adds /gsd-sync-skills command so multi-runtime users can keep gsd-* skill directories aligned across runtime roots after updating one runtime with gsd-update. Changes: - bin/install.js: add --skills-root <runtime> flag that prints the skills root path for any supported runtime, reusing the existing getGlobalDir() table. Banner is suppressed when --skills-root is used (machine-readable output). - commands/gsd/sync-skills.md: slash command definition - get-shit-done/workflows/sync-skills.md: full workflow spec covering argument parsing, path resolution via --skills-root, diff computation (CREATE/UPDATE/ REMOVE/SKIP), dry-run report (default), apply execution, idempotency guarantee, and safety rules (only gsd-* touched, dry-run performs no writes). Safety rules: only gsd-* directories are ever created/updated/removed; non-GSD skills in destination roots are never touched; --dry-run is the default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
5f419c0238 |
fix(bugs): resolve issues #2388, #2431, #2396, #2376 (#2467)
#2388 (plan-phase silently renames feature branch): add explicit Git Branch Invariant section to plan-phase.md prohibiting branch creation/rename/switch during planning; phase slug changes are plan-level only and must not affect the git branch. #2431 (worktree teardown silently swallows errors): replace `git worktree remove --force 2>/dev/null || true` with a lock-aware block in quick.md and execute-phase.md that detects locked worktrees, attempts unlock+retry, and surfaces a user-visible recovery message when removal still fails. #2396 (hardcoded test commands bypass Makefile): add a three-tier test command resolver (project config → Makefile/Justfile → language sniff) in execute-phase.md, verify-phase.md, and audit-fix.md. Makefile with a `test:` target now takes priority over npm/cargo/go. #2376 (OpenCode @$HOME not mapped on Windows): add platform guard in bin/install.js so OpenCode on win32 uses the absolute path instead of `$HOME/...`, which OpenCode does not expand in @file references on Windows. Tests: 29 new assertions across 4 regression test files (all passing). Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
dfa1ecce99 |
fix(#2418,#2399,#2419,#2421): four workflow and installer bug fixes (#2462)
- #2418: convertClaudeToAntigravityContent now replaces bare ~/.claude and $HOME/.claude (no trailing slash) for both global and local installs, eliminating the "unreplaced .claude path reference" warnings in gsd-debugger.md and update.md during Antigravity installs. - #2399: plan-phase workflow gains step 13c that commits PLAN.md files and STATE.md via gsd-sdk query commit when commit_docs is true. Previously commit_docs:true was read but never acted on in plan-phase. - #2419: new-project.md and new-milestone.md now parse agents_installed and missing_agents from the init JSON and warn users clearly when GSD agents are not installed, rather than silently failing with "agent type not found" when trying to spawn gsd-project-researcher subagents. - #2421: gsd-planner.md gains a "Grep gate hygiene" rule immediately after the Nyquist Rule explaining the self-invalidating grep gate anti-pattern and providing comment-stripping alternatives (grep -v, ast-grep). Tests: 4 new test files (30 tests) all passing. Closes #2418 Closes #2399 Closes #2419 Closes #2421 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
0ea443cbcf |
fix(install): chmod sdk dist/cli.js executable; fix context monitor over-reporting (#2460)
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 #2453 Closes #2451 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
f874313807 | fix(install): template bare .claude hook paths for non-Claude runtimes | ||
|
|
af66cd89ca |
fix(install): fatal SDK install failures + CI smoke gate (#2439)
## Why #2386 added `installSdkIfNeeded()` to build @gsd-build/sdk from bundled source and `npm install -g .`, because the npm-published @gsd-build/sdk is intentionally frozen and version-mismatched with get-shit-done-cc. But every failure path in that function was warning-only — including the final `which gsd-sdk` verification. When npm's global bin is off a user's PATH (common on macOS), the installer printed a yellow warning then exited 0. Users saw "install complete" and then every `/gsd-*` command crashed with `command not found: gsd-sdk` (the #2439 symptom). No CI job executed the install path, so this class of regression could ship undetected — existing "install" tests only read bin/install.js as a string. ## What changed **bin/install.js — installSdkIfNeeded() is now transactional** - All build/install failures exit non-zero (not just warn). - Post-install `which gsd-sdk` check is fatal: if the binary landed globally but is off PATH, we exit 1 with a red banner showing the resolved npm bin dir, the user's shell, the target rc file, and the exact `export PATH=…` line to add. - Escape hatch: `GSD_ALLOW_OFF_PATH=1` downgrades off-PATH to exit 2 for users with intentionally restricted PATH who will wire up the binary manually. - Resolver uses POSIX `command -v` via `sh -c` (replaces `which`) so behavior is consistent across sh/bash/zsh/fish. - Factored `resolveGsdSdk()`, `detectShellRc()`, `emitSdkFatal()`. **.github/workflows/install-smoke.yml (new)** - Executes the real install path: `npm pack` → `npm install -g <tgz>` → run installer non-interactively → `command -v gsd-sdk` → run `gsd-sdk --version`. - PRs: path-filtered to installer-adjacent files, ubuntu + Node 22 only. - main/release branches: full matrix (ubuntu+macos × Node 22+24). - Reusable via workflow_call with `ref` input for release gating. **.github/workflows/release.yml — pre-publish gate** - New `install-smoke-rc` and `install-smoke-finalize` jobs invoke the reusable workflow against the release branch. `rc` and `finalize` now `needs: [validate-version, install-smoke-*]`, so a broken SDK install blocks `npm publish`. ## Test plan - Local full suite: 4154/4154 pass - install-smoke.yml will self-validate on this PR (ubuntu+Node22 only) Addresses root cause of #2439 (the per-command pre-flight in #2440 is the complementary defensive layer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
fc1fa9172b |
fix(install): build gsd-sdk from in-repo sdk/ source, not stale npm package
PR #2386 v1 installed the published @gsd-build/sdk from npm, which ships an older version that lacks query handlers needed by current workflows. Every GSD release would drift further from what the installer put on PATH. This commit rewires installSdkIfNeeded() to build from the in-repo sdk/ source tree instead: 1. cd sdk && npm install (build-time deps incl. tsc) 2. npm run build (tsc → sdk/dist/) 3. npm install -g . (global install; gsd-sdk on PATH) Each step is a hard gate — failures warn loudly and point users at the manual equivalent command. No more silent drift between installed SDK and the rest of the GSD system. Root package.json `files` now ships sdk/src, sdk/prompts, sdk/package.json, sdk/package-lock.json, and sdk/tsconfig.json so npm-registry installs also carry the source tree needed to build gsd-sdk locally. Also fixes a blocking tsc error in sdk/src/event-stream.ts:313 — the cast to `Array<{ type: string; [key: string]: unknown }>` needed a double-cast via `unknown` because BetaContentBlock's variants don't carry an index signature. Runtime-neutral type-widening; sdk vitest suite unchanged (1256 passing; the lone failure is a pre-existing integration test that requires external API access). Updates the #1657/#2385 regression test to assert the new build-from-source path (path.resolve(__dirname, '..', 'sdk') + `npm run build` + `npm install -g .`) plus a new assertion that root package.json files array ships sdk source. Refs #2385 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
819af761a0 |
fix(install): verify gsd-sdk resolves on PATH after npm install
`npm install -g` can succeed while the binary lands in a prefix that isn't on the current shell's PATH (common with Homebrew, nvm, or an unconfigured npm prefix). Re-probe via `which gsd-sdk` (or `where` on Windows) after install; if it doesn't resolve, downgrade the success message to a warning with a shell-restart hint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
08b1d8377d |
fix(install): error on mutually exclusive --sdk and --no-sdk flags
Previously passing both silently had --no-sdk win. Exit non-zero with a clear error to match how other exclusive flag pairs (--global/--local, --config-dir/--local) are handled. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b2fcacda1b |
fix(install): auto-install @gsd-build/sdk so gsd-sdk is on PATH (#2385)
Every /gsd-* command shells out to `gsd-sdk query …`, but the SDK was never installed by bin/install.js — the `--sdk` flag documented in README was never implemented. Users upgrading to 1.36+ hit "command not found: gsd-sdk" on every command. - Implement SDK install in finishInstall's finalize path - Default on; --no-sdk to skip; --sdk to force when already present - Idempotent probe via `which gsd-sdk` before reinstalling - Failures are warnings, not fatal — install hint printed Closes #2385 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c35997fb0b |
feat(hooks): add gsd-read-injection-scanner PostToolUse hook (#2201) (#2328)
* 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> |
||
|
|
0da696eb6c |
fix(install): replace all ~/.claude/ paths in Codex .toml files (#2320) (#2325)
* fix(install): replace all ~/.claude/ paths in generated Codex .toml files (#2320) installCodexConfig() only rewrote get-shit-done/-scoped paths; all other ~/.claude/ references (hooks, skills, configDir) leaked into generated .toml files unchanged. Add three additional regex replacements to catch $HOME/.claude/, ~/.claude/, and ./.claude/ patterns and rewrite them to .codex equivalents. Adds regression test PATHS-01. Closes #2320 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(install): handle bare .claude end-of-string and scan all .toml files (CR feedback) - Use capture group (\/|$) so replacements handle both ~/.claude/ and bare ~/.claude at end of string, not just the trailing-slash form - Expand PATHS-01 test to scan agents/*.toml + top-level config.toml - Broaden leak pattern to match ./.claude, ~, and $HOME variants with or without trailing slash Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |