* fix(release-sdk): skip all cherry-pick conflicts in hotfix loop
Full-automation policy: any conflict the cherry-pick can't auto-resolve
— context-missing (#2966) or real merge conflict — is now skipped, not
aborted. The hotfix run completes with whatever applies cleanly; the
SKIPPED list in the run summary becomes the operator's post-hoc review
queue.
Surfaced in run 25227493387 (1.39.1 dry-run): commit 0fb992d
("fix(git): add git.base_branch config") produced real conflicts in
config.cjs / ship.md / complete-milestone.md / tests/config.test.cjs.
v1.39.0 was tagged on the feat/hermes-runtime-2841 branch (#2920),
which restructured those files. 0fb992d was authored against the
pre-restructure shape, so cherry-pick can't auto-resolve.
Pre-#2968 behavior: the workflow distinguished context-missing (skip)
from real (abort + push partial + exit 1). Real conflicts blocked every
hotfix from a base tag whose lineage diverged from main — exactly the
v1.39.x situation. The user has called explicitly for full automation:
"this needs to be fully automated, no one is going to sit there and
tag fixes."
Behavior change:
- Both classification branches now `git cherry-pick --skip` and
append to SKIPPED with a reason category:
* "context absent at base" — empty-HEAD markers (#2966)
* "merge conflict — manual review" — non-empty HEAD (#2968)
- Removed: `git cherry-pick --abort`, partial-state push,
"Cherry-pick conflict" GITHUB_STEP_SUMMARY block, `exit 1`.
- Operator's manual recovery path via `auto_cherry_pick=false`
remains intact.
Trade-off (acknowledged in #2968): a critical fix can be silently
dropped if no one reviews the SKIPPED list. The release job's
install-smoke + full test suite still runs and would catch any
test-covered regression. Fixes that aren't test-covered could ship
missing — accepted cost of full automation per the issue.
Tests:
- tests/bug-2968-cherry-pick-skip-on-any-conflict.test.cjs (new) —
extracts the cherry-pick failure block via bash if/fi nesting walk
(no raw-text grep) and asserts the abort path is removed, --skip
is unconditional, and "merge conflict" + "context absent at base"
annotations both exist.
- tests/bug-2966-cherry-pick-context-missing.test.cjs (renamed
describe + first test name) — assertions still valid since the
classifier survives for skip-reason annotation.
- tests/bug-2964-release-sdk-empty-cherry-pick.test.cjs — unchanged
and still green.
Local: `node --test tests/bug-2964-...test.cjs tests/bug-2966-...test.cjs
tests/bug-2968-...test.cjs` → 8/8 pass.
Local: `npm run lint:tests` → 0 violations.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* fix(release-sdk): split cherry-pick conflict skips from policy skips
CodeRabbit flagged on PR #2970 that conflict skips and policy skips
share the SKIPPED bucket. The run summary heading
"Skipped (feat/refactor/etc — not auto-included)" buries manual-review
conflicts (which the operator must triage) under the same list as
intentional policy exclusions (commits that don't match fix/chore by
design and need no action). Operators reviewing the summary can't
distinguish the two without reading every entry.
Split into two variables:
- POLICY_SKIPPED — feat/refactor/docs/etc filtered out by the
fix/chore regex (informational, no action needed)
- CONFLICT_SKIPPED — fix/chore commits whose cherry-pick failed and
were skipped per the full-automation policy (#2968) (manual
review queue)
Run summary now emits two sections with distinct headings:
- "Skipped — cherry-pick conflict (manual review)"
- "Not auto-included (feat/refactor/docs/etc)"
The new bug-2968 test asserts both buckets are populated correctly:
- failure path appends to CONFLICT_SKIPPED, not SKIPPED
- both bucket variables are echoed in the summary
- both section headings are present
Local: `node --test tests/bug-2964-...test.cjs tests/bug-2966-...test.cjs
tests/bug-2968-...test.cjs` → 9/9 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* fix(release-sdk): handle merge commits and guard cherry-pick --skip
CodeRabbit flagged a real major issue on PR #2970: merge commits with
fix:/chore: titles fail BEFORE entering cherry-pick state because they
need `-m <parent>` to specify the diff base. Without it, the cherry-pick
errors out and CHERRY_PICK_HEAD is never created. The unconditional
`git cherry-pick --skip` call that follows then fails too (no in-progress
cherry-pick to skip), bricking the loop — defeating the full-automation
policy this PR set out to deliver.
Two guards added:
1. Pre-skip merge commits before invoking cherry-pick. The loop checks
parent count via `git rev-list --parents -n 1 "$SHA"`; if > 1, the
commit goes straight to CONFLICT_SKIPPED with reason "merge commit —
manual -m parent selection required". Operator decides which parent
to keep when reviewing the run summary.
2. Guard `git cherry-pick --skip` with a CHERRY_PICK_HEAD existence
check. Catches any other failure mode where the cherry-pick aborts
before entering conflict state (unreadable commit, ref problems,
etc.) so the loop still continues cleanly.
Also bumped the bug-2964 test's regex slice window from 2000 to 4000
chars so the merge-commit pre-skip block doesn't push the cherry-pick
line out of the test's match range.
Tests added in tests/bug-2968-cherry-pick-skip-on-any-conflict.test.cjs:
- merge-commit detection: workflow must call
`git rev-list --parents -n 1 "$SHA"` before cherry-pick and annotate
skips with the distinct "manual -m parent selection required"
reason.
- guard: failure block must check CHERRY_PICK_HEAD before --skip.
Local: `node --test tests/bug-2964-...test.cjs tests/bug-2966-...test.cjs
tests/bug-2968-...test.cjs` → 11/11 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* fix(release-sdk): guard awk classifier against degenerate unmerged paths
CodeRabbit raised two issues on PR #2970:
1. Major (workflow): the `awk` classifier runs under `set -euo pipefail`.
If a CONFLICTED path is missing/unreadable, awk exits non-zero and
terminates the entire step — bricking the loop on a degenerate file.
Also, an unmerged path with no `<<<<<<< ` markers (path-level conflict
or anomalous git state) was misclassified as "context absent at base"
(the auto-skip path), letting potentially-real conflicts skip silently.
Fix: before invoking awk, check `[ ! -r "$CONFLICTED" ]` and
`grep -q '^<<<<<<< ' "$CONFLICTED"`. Either failure marks
ALL_EMPTY_HEAD=false → REASON falls through to "merge conflict —
manual review", landing the pick in the operator review queue.
Also added `2>/dev/null || echo "real"` on the awk call so a
transient awk failure can't slip into the auto-skip bucket.
2. Nitpick (tests): regex assertions on `failureBlock` could match
commented lines (e.g. comment text mentioning "CONFLICT_SKIPPED"
or "git cherry-pick --skip" satisfied the assertions without the
real command being present).
Fix: anchor with `^\s*...` + `m` flag so only executable shell lines
count.
Plus a new test asserting all three workflow guards
(`[ ! -r "$CONFLICTED" ]`, `grep -q '^<<<<<<< '`, `awk ... || echo
"real"`) are present in the failure block.
Local: `node --test tests/bug-2964-...test.cjs tests/bug-2966-...test.cjs
tests/bug-2968-...test.cjs` → 12/12 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* fix(release-sdk): skip cherry-picks whose target context is absent at base
When auto_cherry_pick processed a fix:/chore: commit whose patch modified
code that didn't exist at the hotfix base tag — typically because the
surrounding infrastructure was added later in a feat/refactor commit
excluded by the filter — `git cherry-pick` failed with a conflict that
no operator could meaningfully resolve, and the loop bricked the run.
Discovered re-running the 1.39.1 dry-run after #2965 merged: cherry-pick
of `a3467792` (the #2965 merge itself) failed because the auto_cherry_pick
block it modifies was added in #2956 ("Add automated cherry-pick + SDK-
bundle parity to hotfix flow") — an Add/feat commit, so the fix/chore
filter excludes it. v1.39.0 has no such block, so the patch had no
anchor.
The conflict is unmistakably distinguishable from a real content conflict:
git emits marker blocks where every `<<<<<<< HEAD ... =======` HEAD
section is empty (no anchor lines to reconcile against), while real
conflicts have content on both sides.
After cherry-pick fails:
1. List unmerged paths via `git diff --diff-filter=U`.
2. For each, scan conflict markers with awk. If every HEAD section is
blank/whitespace-only across every block, classify as
context-missing.
3. Context-missing → `git cherry-pick --skip` and append to SKIPPED
list with reason "(context absent at base)".
4. Otherwise fall through to the existing abort/push-partial/error
path that surfaces the conflict for operator resolution.
Real conflicts still surface with the same workflow as before.
Tests in tests/bug-2966-cherry-pick-context-missing.test.cjs cover:
- Static — extracts the "Prepare hotfix branch" run block via
indentation-aware YAML parsing (no raw-text grep) and asserts the
classification predicate, --skip call, and skipped-reason annotation
are present.
- Behavioral — synthetic repo reproducing the real shape of the
failure, asserts cherry-pick exits non-zero and produces the
empty-HEAD marker shape.
- Predicate — pulls the awk script out of the deployed workflow and
feeds it sample conflict shapes (empty-HEAD, real, mixed,
whitespace-only); asserts each is classified as the workflow will
behave.
Local: `node --test tests/bug-2966-...test.cjs` → 3/3 pass.
Local: `npm run lint:tests` → 0 violations.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* fix(release-sdk): pin merge.conflictStyle=merge on hotfix cherry-pick
CodeRabbit flagged on #2967 that the awk classifier introduced for #2966
assumes default conflict-marker style (plain `<<<<<<< HEAD ... ======= ...
>>>>>>>`). If a runner has merge.conflictStyle=diff3 or zdiff3 set
(globally, repo-config, or via git defaults shift), the marker emits an
extra `||||||| ancestor` section between HEAD and =======. The awk's
`in_head` mode would accumulate that ancestor content into the HEAD
buffer, and a context-missing conflict would misclassify as real —
sending the workflow into the abort path on a pick that should be
silently skipped.
Pass `-c merge.conflictStyle=merge` on the cherry-pick command itself
(scoped to that one git invocation; doesn't leak to other commands).
This guarantees marker shape regardless of the runner's git config.
Updated the existing static assertion in
tests/bug-2966-cherry-pick-context-missing.test.cjs to require the pin —
a future edit dropping it fails the test.
Local: `node --test tests/bug-2966-...test.cjs` → 3/3 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* test(#2964): allow git options between `git` and `cherry-pick`
The previous commit on this branch (d6530190) added
`git -c merge.conflictStyle=merge cherry-pick ...` to release-sdk.yml.
The bug-2964 static test's regex `/git cherry-pick[^\n]*"\$SHA"/`
required `cherry-pick` to be the literal next token after `git`, so it
no longer matched the line and CI failed on Node 22 / Node 24 / macOS.
Loosen to `/git\b[^\n]*?cherry-pick[^\n]*"\$SHA"/` so any options
between `git` and `cherry-pick` (e.g. `-c key=value`) are tolerated.
The flag assertions on the matched line still verify --allow-empty and
--keep-redundant-commits are present, which is what bug-2964 actually
guards.
Local: `node --test tests/bug-2964-...test.cjs tests/bug-2966-...test.cjs`
→ 5/5 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* test(#2966): pin merge.conflictStyle in test git wrapper, assert awk status
CodeRabbit raised two issues on PR #2967:
1. The synthetic-repo cherry-pick reproducer asserted `<<<<<<< HEAD ...`
blocks have empty HEAD sections, but the cherry-pick itself didn't
pin `merge.conflictStyle`. A developer or CI runner with global
diff3/zdiff3 config would inject `||||||| ancestor` lines into the
HEAD scan and the test would fail for environment reasons rather
than the bug premise. Pin the style on the test's `git()` wrapper
so every git operation in the test is deterministic regardless of
user config.
2. `classify()` ran awk and consumed `r.stdout.trim()` without checking
`r.status` or `r.error`. A failed awk invocation (missing binary,
syntax error, signal) returns empty stdout, which would falsely
classify as "context-missing" and the test would silently pass on
broken predicates. Add `assert.ok(!r.error, ...)` and
`assert.equal(r.status, 0, ...)` before reading stdout.
Local: `node --test tests/bug-2966-...test.cjs tests/bug-2964-...test.cjs`
→ 5/5 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* 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.
* fix(query/agent-skills): emit raw <agent_skills> block instead of JSON-wrapped string
The CLI dispatcher (`cli.ts`) JSON-stringifies all query handler results via
`console.log(JSON.stringify(result.data, null, 2))`. For the `agent-skills`
handler this produced a JSON-quoted string literal — e.g.
`"<agent_skills>\n…</agent_skills>"` — which workflows embedded verbatim via
`$(gsd-sdk query agent-skills gsd-planner)`, breaking all `<agent_skills>`
injection into spawned subagent prompts.
Fix: add an optional `format: 'json' | 'text'` field to `QueryResult`. When a
handler returns `format: 'text'` and `--pick` is not active, the CLI writes the
string directly via `process.stdout.write` instead of JSON-stringifying it.
`agentSkills` sets `format: 'text'` for non-empty blocks.
Regression guard: two new CLI integration tests in `skills.test.ts` spawn the
CLI as a child process and assert that (a) a mapped agent type receives the raw
XML block on stdout and (b) an unmapped agent type produces the existing JSON
empty-string output.
Fixes#2914.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(changelog): add #2917 entry under Unreleased Fixed
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(workflows): hotfix auto-cherry-pick + SDK-bundle parity (#2955)
hotfix.yml:
- create: auto-cherry-picks fix:/chore: commits from origin/main since
BASE_TAG, oldest-first. Patch-equivalents skipped via git cherry.
feat:/refactor: never auto-included. Conflicts halt with offending SHA.
- finalize: install-smoke gate, sdk-bundle/gsd-sdk.tgz parity with
release-sdk.yml, tightened next dist-tag re-point, --latest on gh
release create. SDK package.json bumped in lockstep.
release-sdk.yml:
- New action input (publish | hotfix) and auto_cherry_pick boolean.
- New prepare job branches hotfix/X.YY.Z from highest vX.YY.* tag,
cherry-picks same logic as hotfix.yml, outputs effective ref.
- install-smoke and release consume prepare.outputs.ref.
- Hotfix mode forces tag=latest, opens merge-back PR. Idempotent if
branch already exists.
VERSIONING.md: documents the cumulative-tag invariant
(vX.YY.Z anchors vX.YY.{Z+1}) and both workflow paths.
Closes#2955
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(code-review): wire --fix dispatch and update stale command references (#2947)
* fix(#2893): surface non-canonical plan filenames instead of silently returning zero plans
Reporter saw `plan_count: 0` from `/gsd:execute-phase` even though five
plan files existed on disk. Investigation showed the planner had written
files like `01-PLAN-01-foundation.md`, while `phase-plan-index`'s strict
filter (`f.endsWith('-PLAN.md') || f === 'PLAN.md'`) rejected them
silently — collapsing two distinct states into the same `plans: []`
return:
- directory truly has no plans (legit empty)
- directory has plans but the filter rejected them (user/agent error)
The canonical contract is documented in three places:
- `agents/gsd-planner.md` write_phase_prompt step (lines 1063-1080)
- `commands/gsd/plan-phase.md`
- `references/universal-anti-patterns.md` (rule 26)
It mandates `{padded_phase}-{NN}-PLAN.md` and explicitly forbids
`PLAN-NN.md` / `01-PLAN-01.md` / `plan-NN.md` etc. The strict filter is
correct per that contract. The bug is that the executor never tells the
user when the contract was violated — they just see `plan_count: 0`
with no signal.
Fix: add a diagnostic helper `describeNonCanonicalPlans()` that scans
the phase directory for files matching `*PLAN*.md` (the diagnostic net)
that the canonical filter rejected, excluding legit derivatives like
`*-PLAN-OUTLINE.md` and `*-PLAN.pre-bounce.md`. When offenders exist,
return a `warning` field naming each one and citing the canonical
pattern so the user knows what to rename to.
Wired into the three filter sites:
- `phase-plan-index` (the executor's main entry point)
- `phases list --type plans`
- `find-phase`
The strict filter itself is unchanged — existing canonical plans behave
identically. This is purely a diagnostic that converts silent-empty
into loud-with-actionable-error.
Tests:
- `phase-plan-index returns warning for reporter's exact filename
pattern (`01-PLAN-01-foundation.md`)`
- `truly empty dir does not emit a warning`
- `canonical plans + outline + pre-bounce files do not emit a warning`
Closes#2893
* test(#2893): add parity tests for find-phase and phases list --type plans warnings
CodeRabbit's only finding on the prior commit: I wired the warning into
three filter sites (`phase-plan-index`, `find-phase`,
`phases list --type plans`) but only `phase-plan-index` had test
coverage for the warning shape. The other two paths could silently
diverge during future refactors — exactly the silent-drift class of bug
this fix exists to prevent.
Add four parity tests mirroring the existing two:
- find-phase: non-canonical filenames produce a warning naming each
offender + citing the canonical pattern.
- find-phase: canonical plan + derivative files (PLAN-OUTLINE,
pre-bounce) produce no warning.
- phases list --type plans: same non-canonical case, but assert the
warning is prefixed with `${dir}: ` (this path aggregates across
phase directories so each offender is tagged with its dir).
- phases list --type plans: canonical case, no warning.
`node --test tests/phase.test.cjs`: 98/98 pass (was 94, +4 new).
* docs(changelog): hotfix flow auto-cherry-pick + SDK bundle parity (#2955)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(workflows): address CodeRabbit findings on hotfix flow (#2955)
5 findings, all real:
1. BASE_TAG selection used lexicographic awk compare, breaking on
multi-digit patches (v1.27.10 wrongly < v1.27.2). Fixed in both
hotfix.yml and release-sdk.yml: append TARGET_TAG to candidate list,
sort -V, take preceding entry. Semver-correct.
2,4. Cherry-pick conflict aborted locally with no remote branch to
resolve from. Now the skeleton branch is pushed up-front (real runs);
on conflict we abort, push the partial-pick state with
--force-with-lease, and emit operator instructions in the run summary.
3. release-sdk.yml dry_run exited before cherry-pick, defeating the
purpose. Now dry_run still applies cherry-picks locally (catches
conflicts), just skips push. Downstream install-smoke runs against
BASE_TAG; the cherry-pick verification itself is the dry-run signal.
5. release-sdk.yml release job missing pull-requests: write — gh pr
create for the merge-back PR would have failed under restricted
token defaults. Permission added.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(workflows): CR round 2 — dry-run signal + post-publish reconciliation (#2955)
3 findings, all real:
6. hotfix.yml create dry_run skipped every step (branch creation,
cherry-pick, version bump) — a green dry-run gave no signal at all.
Now the local checkout/cherry-pick/bump always runs; only the git
push calls are gated on dry_run. Conflicts surface in dry-run too.
7,8. "Refuse if version already on npm" preflight hard-failed reruns,
so a transient failure between npm publish and a later step (tag
push, GH release, merge-back PR, dist-tag re-point) left the release
half-shipped with no path to reconcile. Replaced with a
prior_publish detect step that warns and sets skip_publish=true; the
publish step is gated on that flag, but tag/release/PR/dist-tag
continue. GitHub Release create is now idempotent (edit --latest if
already exists).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(workflows): CR round 3 — preserve dry-run cherry-pick history in conflict guidance (#2955)
Dry-run conflict path discarded successful picks with the runner, but
the message told operators to rerun with auto_cherry_pick=false — which
recreates the branch from BASE_TAG and silently loses every pick that
had succeeded before the conflict.
Updated both hotfix.yml and release-sdk.yml: dry-run conflict summary
now lists the lost SHAs and recommends re-running with
auto_cherry_pick=true (real, not dry-run) to materialize the partial
branch on origin. Real-run guidance unchanged.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(#2948): wire spike --wrap-up flag dispatch
Add dispatch block to commands/gsd/spike.md so that /gsd-spike --wrap-up
routes to the spike-wrap-up workflow instead of silently no-oping. Also
add spike-wrap-up.md to execution_context so the runtime can load it, and
update both companion references in workflows/spike.md from the deleted
/gsd-spike-wrap-up entry-point to /gsd-spike --wrap-up.
Fixes#2948
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#2948): rewrite dispatch test using parseFrontmatter + section extraction
Replace raw fs.readFileSync + text.includes() / regex assertions with structural
parsing: parseFrontmatter extracts the YAML frontmatter fields and _body,
extractSection pulls named XML blocks, and parseExecutionContextRefs resolves
the @-prefixed workflow references. Assertions now target the argument-hint
frontmatter field, the execution_context @-ref list, and the routing text within
<context>/<process> sections — not arbitrary substrings in the raw file.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#2948): tighten dispatch assertion to line-level rule check
Replace the co-occurrence check (dispatchText.includes('--wrap-up') &&
dispatchText.includes('spike-wrap-up')) with line-level assertions that parse
the <process> section's rules array, find the exact '- If it is `--wrap-up`:'
line, verify it includes 'strip the flag' and 'spike-wrap-up', and assert the
'- Otherwise:' fallback still routes to the spike workflow.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#2948): anchor parseFrontmatter to line 0 to avoid mid-file --- delimiters
parseFrontmatter was scanning the whole file for the first two '---' lines,
which can match a mid-document horizontal rule as the opening delimiter.
Now requires lines[0].trim() === '---'; returns { _body: content } for files
with no frontmatter, and searches for the closing '---' from line 1 onward.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2950): update stale deleted-command references in workflow files
Eight workflow files (help.md, do.md, settings.md, discuss-phase.md,
new-project.md, plan-phase.md, spike.md, sketch.md) referenced command
names removed in #2790. Updated all occurrences to canonical new forms:
/gsd-phase (--insert / --remove), /gsd-capture, /gsd-config (--profile
/ --integrations / --advanced), /gsd-spike --wrap-up,
/gsd-sketch --wrap-up, /gsd-code-review --fix.
Adds regression test (124 assertions) in tests/bug-2950-stale-command-refs.test.cjs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#2950): update pre-existing assertions to accept new consolidated command forms
gsd-settings-advanced.test.cjs and settings-integrations.test.cjs were checking
settings.md for the old micro-skill names (/gsd-settings-advanced,
/gsd-settings-integrations). Now that #2950 updates settings.md to use the
consolidated equivalents, broaden the assertions to accept both old and new forms.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(#2950): require canonical command forms and forbid legacy variants
The broadened OR assertions added to unblock CI were too permissive — they
could pass with legacy names still present. Now assert the canonical form is
present (gsd-config --advanced / gsd-config --integrations) AND the legacy
forms are absent (gsd-settings-advanced, gsd:settings-advanced,
/gsd-settings-integrations).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2949): wire sketch --wrap-up flag dispatch
Add dispatch logic to commands/gsd/sketch.md so --wrap-up routes to the
sketch-wrap-up workflow instead of silently falling through to the normal
sketch workflow. Also adds sketch-wrap-up.md to execution_context and
updates companion references in workflows/sketch.md from the deleted
/gsd-sketch-wrap-up command to /gsd-sketch --wrap-up.
Fixes#2949
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2949): use exact-match "If it is" instead of "If it contains" for --wrap-up dispatch
Aligns with the established pattern across all consolidated commands
(workspace.md, update.md, progress.md) where the first-token check uses
"If it is `--flag`" for exact equality, not substring matching.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(#2893): surface non-canonical plan filenames instead of silently returning zero plans
Reporter saw `plan_count: 0` from `/gsd:execute-phase` even though five
plan files existed on disk. Investigation showed the planner had written
files like `01-PLAN-01-foundation.md`, while `phase-plan-index`'s strict
filter (`f.endsWith('-PLAN.md') || f === 'PLAN.md'`) rejected them
silently — collapsing two distinct states into the same `plans: []`
return:
- directory truly has no plans (legit empty)
- directory has plans but the filter rejected them (user/agent error)
The canonical contract is documented in three places:
- `agents/gsd-planner.md` write_phase_prompt step (lines 1063-1080)
- `commands/gsd/plan-phase.md`
- `references/universal-anti-patterns.md` (rule 26)
It mandates `{padded_phase}-{NN}-PLAN.md` and explicitly forbids
`PLAN-NN.md` / `01-PLAN-01.md` / `plan-NN.md` etc. The strict filter is
correct per that contract. The bug is that the executor never tells the
user when the contract was violated — they just see `plan_count: 0`
with no signal.
Fix: add a diagnostic helper `describeNonCanonicalPlans()` that scans
the phase directory for files matching `*PLAN*.md` (the diagnostic net)
that the canonical filter rejected, excluding legit derivatives like
`*-PLAN-OUTLINE.md` and `*-PLAN.pre-bounce.md`. When offenders exist,
return a `warning` field naming each one and citing the canonical
pattern so the user knows what to rename to.
Wired into the three filter sites:
- `phase-plan-index` (the executor's main entry point)
- `phases list --type plans`
- `find-phase`
The strict filter itself is unchanged — existing canonical plans behave
identically. This is purely a diagnostic that converts silent-empty
into loud-with-actionable-error.
Tests:
- `phase-plan-index returns warning for reporter's exact filename
pattern (`01-PLAN-01-foundation.md`)`
- `truly empty dir does not emit a warning`
- `canonical plans + outline + pre-bounce files do not emit a warning`
Closes#2893
* test(#2893): add parity tests for find-phase and phases list --type plans warnings
CodeRabbit's only finding on the prior commit: I wired the warning into
three filter sites (`phase-plan-index`, `find-phase`,
`phases list --type plans`) but only `phase-plan-index` had test
coverage for the warning shape. The other two paths could silently
diverge during future refactors — exactly the silent-drift class of bug
this fix exists to prevent.
Add four parity tests mirroring the existing two:
- find-phase: non-canonical filenames produce a warning naming each
offender + citing the canonical pattern.
- find-phase: canonical plan + derivative files (PLAN-OUTLINE,
pre-bounce) produce no warning.
- phases list --type plans: same non-canonical case, but assert the
warning is prefixed with `${dir}: ` (this path aggregates across
phase directories so each offender is tagged with its dir).
- phases list --type plans: canonical case, no warning.
`node --test tests/phase.test.cjs`: 98/98 pass (was 94, +4 new).
* feat(workflows): add atomic Write+commit ordering directive for SUMMARY.md
Adds explicit prompt-ordering language to executor spawn prompts and
plan-execution steps so agents commit SUMMARY.md before emitting any
concluding narrative. Mitigates the truncation-between-Write-and-commit
failure mode that has made the #2070 rescue net load-bearing.
Refs #2806
* fix(workflows): condense REQUIRED ORDER blocks to fit XL budget
The two REQUIRED ORDER directives added in bd1956df pushed
execute-phase.md to 1712 lines, exceeding the 1700-line XL budget.
Collapse each 6-line block into a single line that preserves the
semantic intent (Write SUMMARY.md → commit → narration; no text
between Write and commit; #2070 rescue is not primary defense).
File is now exactly 1700 lines; workflow-size-budget test passes.
* fix(execute-plan): move self-check before commit to preserve atomic Write+commit (#2939)
* 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.
* fix(workflows): assert HEAD on per-agent branch before worktree commits
Worktree-mode setup could leave HEAD attached to a protected branch (master),
causing agent commits to land there. The previous response was a destructive
self-recovery via 'git update-ref refs/heads/master <sha>', which silently
rewinds the protected branch and destroys concurrent commits in multi-active
scenarios (parallel agents, user committing while agent runs).
- Reorder <worktree_branch_check> in execute-phase.md and quick.md to assert
HEAD via 'git symbolic-ref' BEFORE any 'git reset --hard'. HALT with a
blocker if HEAD is on main/master/develop/trunk/release/* or detached.
- Add a per-commit HEAD assertion (step 0) to gsd-executor.md
<task_commit_protocol>; HEAD attachment can drift after 'git checkout <sha>'.
- Forbid 'git update-ref refs/heads/<protected>' in
<destructive_git_prohibition>; surface the blocker rather than self-heal.
- Remove '--no-verify' as the worktree-mode default in execute-phase.md,
execute-plan.md, quick.md, and references/git-integration.md. Hooks now
run on every executor commit; opt out only via workflow.worktree_skip_hooks.
- Add regression test that parses the worktree_branch_check blocks structurally
and asserts the symbolic-ref check precedes the reset --hard, no workflow
performs update-ref on a protected ref, and --no-verify is no longer the
default in any parallel-execution prompt.
* fix(#2924): address CodeRabbit review findings on worktree HEAD PR
- Add positive worktree-agent-* allow-list to <task_commit_protocol> step 0
in gsd-executor.md and to <worktree_branch_check> in execute-phase.md and
quick.md. The deny-list (main|master|develop|trunk|release/*) silently
allowed feature/* and other arbitrary branches outside the agent namespace.
- Register workflow.worktree_skip_hooks in both config schemas
(sdk/src/query/config-schema.ts and get-shit-done/bin/lib/config-schema.cjs)
and document it in docs/CONFIGURATION.md so config-set accepts it.
- Fix stash lifecycle in execute-phase.md post-wave hook validation: stash
under a named ref and pop after the hook run; warn on pop failure.
- Pre-dispatch PLAN.md commit in quick.md: gate on git diff --cached --quiet
for idempotency and exit 1 with a clear error on commit failure (both the
--no-verify and the normal branches) — no more swallowing real errors.
- Test fixes (tests/bug-2924-worktree-head-attachment.test.cjs):
- Parse the protected-branch alternation structurally and require
main, master, develop, trunk, release/.* (release/* was previously
skipped by the \\b...\\b regex).
- Use fs.readdirSync(dir, { recursive: true }) so workflows in nested
subdirectories are also asserted against the update-ref ban.
- Add allow-list assertions for execute-phase.md, quick.md, and
gsd-executor.md to lock in the new positive namespace check.
* test(#2924): assert sub-section end marker exists before slicing
* test(#2924): use section boundary instead of fixed window for parallel-agents slice
* fix(config-get): return schema default for context_window when absent (#2943)
cmdConfigGet in bin/lib/config.cjs now consults a SCHEMA_DEFAULTS map before
emitting "Key not found", so context_window (and any future schema-defaulted
keys) return their default value (exit 0) when not set in config.json.
Also updates the stale subagent-timeout.test.cjs assertion that expected the
old broken behavior (exit 1 / "Key not found") to match the corrected behavior.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: use distinct sentinel to prove --default wins over schema default (#2943)
* docs: update CHANGELOG.md for #2943 fix
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
After v1.39.0 skill consolidation (#2790), skills/ became a GSD-managed
root that the installer wipes on update. GSD_MANAGED_DIRS in gsd-tools.cjs
was missing 'skills', so user-added skill directories (e.g.
skills/custom-skill/SKILL.md) were never walked and silently destroyed
during /gsd-update.
- Add 'skills' to GSD_MANAGED_DIRS so the directory is walked
- Add tests/bug-2942-detect-custom-skills.test.cjs with 5 targeted tests
- Update tests/update-custom-backup.test.cjs: replace the now-incorrect
"skills/ must NOT be scanned" assertion (written pre-#2790) with a test
that verifies custom skills ARE detected and GSD-owned skills are not
falsely flagged
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces stale v1.32/v1.37 highlight blocks with v1.39.0 highlights in
README.md and four translations, adds /gsd-edit-phase to phase-management
tables, documents workstream config inheritance, the post-merge build gate,
and per-runtime review.models.<cli> selection.
Closes#2935
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.
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.
CodeRabbit pointed out the post-creation guard is structurally
unreachable: immediately after `git checkout -b X origin/$DEFAULT_BRANCH`,
HEAD == origin/$DEFAULT_BRANCH, so both the merge-base form (`MB == DT`)
and the alternative "ahead-of" count form (`AHEAD == 0`) are sentinels
that always pass on a successful fresh checkout. With the explicit base
arg + fail-fast on the checkout, the guard cannot catch anything new.
Removing it (rather than swapping in another no-op that satisfies the
linter but adds no actual coverage) is the honest fix. Comment retained
to explain why no post-creation guard is needed: the explicit base
argument to `git checkout -b` is the single source of correctness for
#2916.
Same simplification mirrored in get-shit-done/workflows/quick.md.
Full suite: 6102/6102.
Two CodeRabbit findings on PR #2921 (review 4209533909 + comment
3171721073, both still unresolved):
A. Branch switch and create steps now abort on non-zero exit. Previously
`git switch "$BRANCH_NAME"` and `git checkout -b "$BRANCH_NAME"
"origin/$DEFAULT_BRANCH"` could fail (locked worktree, dirty tree
refusing the checkout, etc.) and the workflow would silently continue
on the wrong branch — sending the phase's later commits to the wrong
place. Both calls now `|| { echo "ERROR: …" >&2; exit 1; }`.
B. The fork-point base-warning is now scoped to the creation arm of
the if/else. Previously it ran for the resume path too, so a
legitimate resumed branch where origin/$DEFAULT_BRANCH had advanced
since first creation would falsely warn ("does not fork from
origin/<DEFAULT_BRANCH>"). Moving the check inside the else arm
means it only runs immediately after a fresh `git checkout -b`, when
the merge-base check is meaningful.
Same fix mirrored in get-shit-done/workflows/quick.md.
execute-phase.md stays at the 1700-line XL budget. Full suite: 6102/6102.
Two follow-ups on commit 80f14cac (which hardened quick-branching with a
trunk fixture):
1. quick-branching.test.cjs: add a `defaultBranch` parameter to
setupFixture and run the "branches off origin/HEAD" assertion against
both `main` and `trunk`. The wholesale switch to trunk in 80f14cac
removed coverage of the conventional `main` path; parameterizing
restores it without giving up the symbolic-ref guarantee.
2. bug-2916-handle-branching-default-base.test.cjs: apply the same
parameterization here. handle_branching has the same default-branch
detection logic as Step 2.5, so it deserves the same trunk regression
guard. Previously this file only exercised `main`.
A regression that silently defaults to `main` instead of consulting
`git symbolic-ref refs/remotes/origin/HEAD` now fails the `trunk`
variant in both files.
Tests: 10/10 in the touched suites.
- Restrict the "init parse list includes branch_name" assertion to
the bash blocks inside Step 2 (Initialize) so an unrelated step
that mentions branch_name cannot mask the contract.
- Switch the fixture's default branch from main to trunk so the
symbolic-ref code path is locked in: a regression that silently
defaults to "main" instead of consulting origin/HEAD now fails.
Addresses CodeRabbit review on PR #2921.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the "ahead-of" heuristic with a structural check that compares
the HEAD↔origin/$DEFAULT_BRANCH merge-base to origin/$DEFAULT_BRANCH
itself. The previous count-based warning fired on legitimate WIP that
was simply ahead of the default branch — the correct signal is that
the branch did not fork from the default branch in the first place.
Addresses CodeRabbit review on PR #2921.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two test files were asserting installer prompt behavior by regex/.includes()
against bin/install.js source. Per CONTRIBUTING.md "no-source-grep"
testing standard, replace with structured assertions:
- tests/kilo-install.test.cjs: import runtimeMap and buildRuntimePromptText
from the install module; assert runtimeMap['11'] === 'kilo' and that the
rendered prompt lists Kilo above OpenCode without marketing copy.
- tests/multi-runtime-select.test.cjs: import runtimeMap, allRuntimes,
parseRuntimeInput, buildRuntimePromptText. Assert exported runtimeMap
matches the canonical option list, allRuntimes contains every runtime
exactly once, prompt text lists Hermes (10), Qwen Code (13), Trae (14),
All (16), and parser splits/dedupes by exercising parseRuntimeInput
rather than regexing source code.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
CodeRabbit nitpick (per project policy `feedback_no_source_grep_tests`):
the prior `tests/quick-branching.test.cjs` asserted branching correctness
by `.includes()`-grepping the raw markdown content for literal command
substrings. Those assertions stayed green even when the underlying
behavior regressed (e.g. when `git checkout -b` was unconditionally run
from the wrong HEAD).
Replace with the same pattern as `bug-2916-handle-branching-default-base
.test.cjs`:
- Structurally extract the Step 2.5 bash block from quick.md by
walking the markdown for fenced ```bash blocks under the heading
(no regex on prose).
- Spin up a fixture git repo with a bare origin, a clone whose
`origin/HEAD` points at `main`, and a checked-out previous-task
branch carrying its own unmerged commit.
- Execute the extracted bash block via `bash -c` and assert that
the new branch's tip equals `origin/main` (0 commits inherited
from the previous-task HEAD).
- Add a reuse test that pre-creates the target branch with its own
commit and verifies the script switches back to it without a
rebase or reset.
The two informational tests (workflow file exists, branching runs
before task-directory creation) are retained, plus the `branch_name`
parsing assertion is rewritten to walk fenced bash blocks rather than
substring-grep arbitrary content.
Address CodeRabbit HIGH findings on PR #2921. The previous fix had three
unconditional code paths where `git checkout -b "$BRANCH_NAME"` would run
from the *current* HEAD when the upstream sync failed silently:
- the dirty-tree warn-and-continue path,
- the clean path where `git switch` / `git merge --ff-only` errors were
swallowed by `2>/dev/null` (still falling through to checkout -b),
- any case where `git fetch` failed but the script continued.
This rewrites both `execute-phase.md` (handle_branching) and `quick.md`
(Step 2.5) to:
1. Fetch origin/$DEFAULT_BRANCH; if fetch fails AND no local copy of
origin/$DEFAULT_BRANCH exists, abort with a clear ERROR (exit 1)
rather than create the branch off arbitrary HEAD.
2. Always create the new branch with an explicit start point:
`git checkout -b "$BRANCH_NAME" "origin/$DEFAULT_BRANCH"`. The base
is now deterministic regardless of which branch is currently
checked out, regardless of whether the optional local fast-forward
succeeded, and regardless of dirty-tree state.
3. Carry uncommitted changes onto the new (origin-pinned) branch
instead of inheriting the previous-phase HEAD as a fallback base.
The post-creation INHERITED check now references origin/$DEFAULT_BRANCH
rather than the (possibly-stale) local default branch, so the warning
fires accurately even when the local fast-forward was skipped.
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).
The docstring coverage pre-merge check (default: warning at 80% threshold)
produces false-positive warnings on PRs whose new code is entirely test
files: it counts test(...) / beforeEach / afterEach arrow-function
callbacks as functions and reports 0% coverage because nothing has JSDoc.
CR's documented schema for reviews.pre_merge_checks.docstrings only
accepts `mode` and `threshold` — there is no per-check path filter that
would let us exclude tests/** while keeping the check active elsewhere.
The top-level path_filters approach would silence ALL CR review on test
files (security scans, out-of-scope checks, the substantive line-level
findings) which we want to keep.
Disabling the check entirely is the right call for this repo because:
- GSD ships a CLI + agent runtime, not a documented public library
- The internal helpers that warrant JSDoc already have it
- The other CR pre-merge checks (out-of-scope, security, title) are
meaningful for this codebase and stay enabled
Closes#2932
Two existing tests called extractBlockquotes(reportStep) without first
asserting reportStep was non-null. If the workflow file ever loses its
`<step name="report">` block, the test would fail with a confusing
TypeError on the destructuring inside extractBlockquotes instead of a
clear "report step must exist" assertion.
Add assert.ok(reportStep, ...) guards at the two missing call sites
(lines 100 and 130). The other two call sites (lines 75-83) already
had guards.
Addresses CodeRabbit comment on PR #2918.
Ports the pre-publish CI gates that release.yml applies into release-sdk.yml,
so the stopgap workflow ships releases at the same quality bar as the
canonical workflow (minus the @gsd-build/sdk publish, still intentionally
omitted, and the release-branch ceremony, intentionally omitted).
Changes (all mechanical copies of release.yml patterns):
- install-smoke as needs: dependency. The reusable workflow at
.github/workflows/install-smoke.yml runs the cross-platform install
matrix (Ubuntu 22/24, macOS 24, packed-vs-unpacked). Publish job
won't start until install-smoke passes for the dispatched ref.
- npm test → npm run test:coverage. Full coverage gate, matching
release.yml's pre-publish test step.
- Tolerant tag-existence check. The previous upfront "refuse if tag
exists" was too strict — operators re-running after a mid-flight
publish-step failure would be blocked by the tag they successfully
pushed last time. New behavior matches release.yml: skip the tag
step if the tag points at HEAD; error only if it points elsewhere.
- Tag-and-push step gets the same skip-if-at-HEAD pattern.
- New "Re-point next dist-tag at the new latest" step, gated on
tag=latest. Matches release.yml#finalize "Clean up next dist-tag" —
keeps @next from going stale relative to @latest.
- New "Create GitHub Release" step. Per-tag flag selection:
tag=dev, tag=next → --prerelease (won't be highlighted on repo home)
tag=latest → --latest (becomes the highlighted release)
All use --generate-notes so the release body auto-fills from commits.
- Summary updated to mention the GitHub Release and dist-tag re-point.
Out of scope per #2929:
- canary.yml, release.yml unchanged (verified by file diff)
- bin/install.js unchanged (install path already uses bundled SDK)
- No @gsd-build/sdk publish anywhere
- No release/X.Y.Z branch ceremony (this stopgap targets dispatched
ref directly)
Adds a workflow_dispatch-only release path that publishes get-shit-done-cc
to ONE chosen dist-tag per run (dev | next | latest), with the SDK
bundled inside the CC tarball both as the existing loose sdk/dist/ tree
and as a fresh sdk-bundle/gsd-sdk.tgz npm-installable artifact.
Why: @gsd-build/sdk publishes from canary.yml and release.yml fail because
the @gsd-build npm token is currently unavailable. CC users don't consume
@gsd-build/sdk directly — bin/gsd-sdk.js resolves sdk/dist/cli.js from
inside the installed CC package. This workflow ships only get-shit-done-cc
(which we hold the token for) and bundles the SDK two ways so any future
install path can pick whichever shape it needs.
The new sdk-bundle/ directory is added to the CC files whitelist in-tree
at build time only — never committed. Existing canary.yml and release.yml
are intentionally untouched; restore them to primary use once the
@gsd-build/sdk token is recovered.
Per-tag version derivation when the version input is empty:
- dev → <base>-dev.N (next sequential, scanning v<base>-dev.* tags)
- next → <base>-rc.N (matches release.yml convention)
- latest → <base> (clean, no suffix)
Refuses to publish when the version already exists on npm or has an
existing git tag (no accidental overwrites). Verifies the publish landed
on the registry and the dist-tag resolves correctly before marking the
run successful.
handle_branching in execute-phase.md (and the equivalent step in quick.md)
created the per-phase branch from whatever branch happened to be checked
out — typically the previous phase's still-unmerged feature branch — so
consecutive phases compounded on top of each other and stayed unpushed.
Detect the default branch via git symbolic-ref refs/remotes/origin/HEAD,
fast-forward it from origin, and fork the new phase branch off that tip.
Existing branches are still reused as-is. Dirty working trees fall back
to current HEAD with a loud warning, and a post-creation guard reports
any inherited commits.
Regression test extracts the bash from the <step name="handle_branching">
block structurally and runs it against a fixture repo where HEAD sits on
a previous-phase branch with extra commits.
Two bugs in the audit-open dispatch case in bin/gsd-tools.cjs:
1. Bare output(...) calls (only core.output is in scope) threw
ReferenceError: output is not defined on every invocation,
blocking the first step of /gsd-complete-milestone.
2. Even after switching to core.output(formattedReport, raw), the
human-readable branch JSON-stringified the formatted text because
core.output only bypasses JSON encoding when called as
core.output(null, true, rawValue).
Fix:
- --json path: core.output(result, raw) — pass the object,
let core.output JSON-stringify (don't pre-stringify).
- text path: core.output(null, true, formatAuditReport(result))
— use the rawValue form to emit verbatim section dividers and
item lists.
Adds tests/bug-2911-audit-open-output-shape.test.cjs which parses
both modes structurally — line-by-line for text mode (asserting the
report headers exist as standalone lines, not as escaped \n inside a
JSON quoted string), and JSON.parse + key-by-key shape assertions for
--json mode (matching the contract returned by auditOpenArtifacts).
The report step in workflows/progress.md had no directive establishing
PROJECT.md/STATE.md/ROADMAP.md as the authoritative sources for the
progress report. When init.progress returned project_exists: false (e.g.
invoked from a subdirectory without .planning/), the model fell back to
whatever was in its session context — including stale CLAUDE.md
## Project blocks — and produced routing output citing the wrong
milestone/phase.
Add a blockquote directive at the top of the report step that names
PROJECT.md, STATE.md, and ROADMAP.md as authoritative and forbids using
the CLAUDE.md ## Project block as a source for any progress report field.
Fixes#2912
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)
* fix: parse non-REQ IDs in gap-analysis and ignore table headers
* fix: parse requirement IDs from first traceability column only
---------
Co-authored-by: Tom Boucher <thomas.boucher@sas.com>