Merge origin/main into dev for v1.50.0-canary.2 prep

Brings dev current with main's latest stable + RC stream so the next canary
build reflects main + the MVP umbrella (#2826) work landed on dev:

  - Phase 1 (#2885) — --mvp flag, planner machinery, Walking Skeleton
  - Phase 2 (#2875) — /gsd mvp-phase command + SPIDR + user-story PLAN.md
  - Phase 3a (#2877) — execute-phase MVP+TDD runtime gate
  - Phase 3b (#2879) — verify-work MVP-mode UAT framing
  - Phase 4 (#2882) — new-project mode prompt + progress/stats/graphify

Conflict resolutions:
  - CHANGELOG.md — kept dev's MVP entries, repointed under main's typed
    "### Feature" header (Keep-a-Changelog → typed migration on main).
  - docs/INVENTORY-MANIFEST.json — regenerated from filesystem; took main's
    "generated" date; merged references list.
  - docs/INVENTORY.md — recounted from filesystem (commands 66, workflows
    88, references 58), kept dev's mvp-phase row, took main's
    "/gsd-progress --do" invocation form for do.md.
  - get-shit-done/workflows/plan-phase.md — combined --research-phase <N>
    parser block (main, #3042/#3044) with --mvp flag (dev).
  - .github/workflows/release-sdk.yml — took main's hotfix-action version
    wholesale; dev's copy was byte-identical to a prior main snapshot
    (4d534cf0) with no dev-specific intent to preserve.
  - sdk/package-lock.json — took main's version.

No open PRs target dev. Force-push not used; this is a true merge with a
new merge commit on top of origin/dev.
This commit is contained in:
Tom Boucher
2026-05-06 12:54:26 -04:00
745 changed files with 37385 additions and 7458 deletions

View File

@@ -0,0 +1,4 @@
---
type: Fixed
---
**`/gsd-graphify build` now runs inline instead of spawning a sub-agent (#3166)** — graphify v0.7+ split the build into a fast AST-extraction phase (cached) followed by a separate clustering + report-write phase. The cached extraction phase survived sub-agent isolation, but the post-extraction phase was SIGTERM'd when the agent exited, leaving the cache populated and no `graph.json` / `graph.html` / `GRAPH_REPORT.md` artifacts written to `.planning/graphs/`. The skill now runs `graphify update .`, the three artifact copies, the snapshot, and the status report as a single foreground Bash call so the entire pipeline survives to completion. The CLI's `graphify build` pre-flight still returns `action: "spawn_agent"` so external callers and existing tests keep working. Adds a structural regression test parsing the skill's YAML frontmatter to fence against re-introducing `Task` to `allowed-tools`.

View File

@@ -0,0 +1,4 @@
---
type: Enhancement
---
**`/gsd-graphify status` surfaces graphify v0.7+ commit-based staleness (#3170)** — `graphifyStatus()` now reads `built_at_commit` from `graph.json` (written by graphify v0.7+ at build time), compares it against `git HEAD`, and returns four new fields: `built_at_commit`, `current_commit`, `commits_behind`, and `commit_stale`. The `commit_stale` flag is tri-state — `true` / `false` / `null`, where `null` means the signal is unavailable (pre-v0.7 graph, non-git checkout, or unreachable commit) and callers should fall back to the existing mtime-based `stale` flag. The skill renders `Source commit: <hash> (N commits behind HEAD | current | freshness unknown)` when the signal is present, and omits the line entirely for pre-v0.7 graphs. The `built_at_commit` value is validated as 440 hex chars before reaching `git`, so a hostile `graph.json` cannot smuggle dashed options (e.g. `--upload-pack=…`) into the argv. Also documents `graphify hook install` in `docs/CONFIGURATION.md` for multi-dev teams who would otherwise hit `graph.json` merge conflicts on parallel rebuilds.

44
.changeset/README.md Normal file
View File

@@ -0,0 +1,44 @@
# Changeset Fragments
This directory holds **per-PR CHANGELOG fragments**. Every PR with user-facing changes drops one (or more) `<random-name>.md` files here describing its CHANGELOG entry. Fragments are consolidated into the top-level `CHANGELOG.md` at release time.
## Why
Two PRs that both edit the `### Fixed` block of `CHANGELOG.md` always conflict on merge — git can't pick a serialization order without human input. Two PRs that each add a fresh `.changeset/<unique-name>.md` never conflict because they don't share lines.
See [#2975](https://github.com/gsd-build/get-shit-done/issues/2975) for the full rationale.
## Adding a fragment
```bash
node scripts/changeset/new.cjs \
--type Fixed \
--pr 1234 \
--body "fix the thing — explain the user-visible change in one sentence"
```
This writes `.changeset/<adjective>-<noun>-<noun>.md` with frontmatter and a body. Three random words → concurrent PRs don't collide.
## Format
```md
---
type: Fixed
pr: 1234
---
**`/gsd-foo` no longer drops trailing slashes** — explain the user-visible change.
```
Allowed `type:` values follow [Keep a Changelog](https://keepachangelog.com/): `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed`, `Security`.
## Opting out
PRs that legitimately have no user-facing impact can add the `no-changelog` label. CI honors it. When unsure, add the fragment.
## At release time
```bash
node scripts/changeset/cli.cjs render --version vX.Y.Z --date YYYY-MM-DD
```
Reads every fragment, groups bullets by `type:`, replaces `## [Unreleased]` with a new `## [vX.Y.Z] - YYYY-MM-DD` block, opens a fresh `## [Unreleased]` above, deletes consumed fragments. Idempotent.

View File

@@ -0,0 +1,11 @@
---
type: Changed
pr: 3152
---
**Command contract validation now enforced in CI (ADR-0002)** — \`scripts/lint-command-contract.cjs\` runs as a pre-test step and validates every \`commands/gsd/*.md\` file against five rules: \`name:\` present + \`gsd:\` prefix, \`description:\` non-empty, \`allowed-tools:\` entries canonical, \`execution_context\` @-refs resolve on disk, @-refs on their own line. Prevents the \`add-backlog.md\`-class gap from silently reappearing on consolidation PRs.
**~900 tokens/invocation recovered** — prose \`@~/.claude/get-shit-done/...\` path tokens removed from \`<process>\` blocks in 39 command files. The \`<execution_context>\` block is now the single authoritative load declaration; the duplicate prose copies were inert but consumed context on every command invocation.
**~3,750 tokens removed from eager session load** — \`/gsd-debug\` (9,603 → 1,703 chars) and \`/gsd-thread\` (7,868 → 585 chars) now follow the workflow-delegation pattern used by all other commands. Their implementations moved to \`get-shit-done/workflows/debug.md\` and \`get-shit-done/workflows/thread.md\`. Behavior is unchanged.
\`get-shit-done/workflows/extract_learnings.md\` renamed to \`extract-learnings.md\` to match the hyphen convention of all other workflow files. Closes #3151.

View File

@@ -0,0 +1,5 @@
---
type: Changed
---
**Query command dispatch deepened with Command Topology Module** — query dispatch now consumes a single topology seam that resolves command tokens, binds native handler adapters, and returns structured no-match diagnosis, improving locality and reducing dispatch seam drift.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3058
---
**GSD transport raw-mode handling and timeout fallback hardened** — fixes undefined raw formatting edge case and adds raw-path coverage to prevent regressions.

View File

@@ -0,0 +1,8 @@
---
type: Changed
pr: 3069
---
**query command metadata now flows through a canonical Command Definition Module seam** — registry assembly, mutation semantics, and alias generation consume one Interface (`family`, `canonical`, `aliases`, `mutation`, `output_mode`, `handler_key`) to improve locality and reduce drift.
**query fallback error mapping cleanup** — the CJS fallback catch path now passes original `err` to `mapFallbackDispatchError` (follow-up to prior review feedback missed in PR #3066).

View File

@@ -0,0 +1,6 @@
---
type: Changed
pr: 3075
---
**query architecture deepening pass** — extracted Query Runtime Context, Native Dispatch Adapter, and Query CLI Output Modules so dispatch policy, runtime context policy, and CLI projection logic each live behind focused seams with higher locality and leverage.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2990
---
gsd-code-fixer worktree no longer fails on the same-branch checkout — the agent now creates a new gsd-reviewfix/ branch via git worktree add -b and fast-forwards the user's branch on cleanup. See #2990.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 2986
---
Test suite for config-schema.cjs is now mutation-resistant — 95 typed assertions kill the 124 surviving Stryker mutants from the 4.62% baseline. Tests target static-key fast path, dynamic-pattern .some semantics, polarity, and regex-anchor tightening. See #2986.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3008
---
**`tests/install-minimal.test.cjs:307` no longer races on shared `os.tmpdir()` under parallel CI** — the previous shape compared `listTmpStageDirs()` snapshots before and after the throw. Under `scripts/run-tests.cjs --test-concurrency=4`, `tests/install-minimal-all-runtimes.test.cjs` runs in a parallel process and creates/removes `gsd-minimal-skills-*` dirs in the shared OS tmpdir between snapshots, so `deepStrictEqual` failed deterministically when the parallel process happened to have a live stage dir during the snapshot window. Fix: stub `fs.mkdtempSync` to record THIS call's stage dir, then assert that exact path no longer exists after the throw — no global filesystem snapshot, no race. (#3008)

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3022
---
**Codex SessionStart hook now uses absolute Node binary path** — closes the gap left after #3002. The Codex install path wrote `command = "node ${path}"` directly into config.toml, bypassing `resolveNodeRunner()`. Under GUI/minimal-PATH runtimes (`/usr/bin:/bin:/usr/sbin:/sbin`), bare `node` failed to resolve, exit 127. Now routed through new `buildCodexHookBlock()` helper. Reinstall path migrates legacy bare-node entries via new `rewriteLegacyCodexHookBlock()`. See #3017.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: TBD
---
**Codex skill adapter no longer instructs the agent to silently default discuss-phase decisions.** When `request_user_input` was rejected (Default mode), the generated adapter said "pick a reasonable default" — so `$gsd-discuss-phase` proceeded toward writing CONTEXT.md / DISCUSSION-LOG.md / checkpoints without ever asking the user. Adapter prose now requires the agent to STOP, present plain-text questions, and wait, with explicit named exceptions (`--auto`/`--all`/explicit user approval). See #3018.

View File

@@ -0,0 +1,6 @@
---
type: Changed
pr: 3074
---
**query CLI path extracted into a dedicated Query CLI Adapter Module**`sdk/src/cli.ts` now delegates query-specific dispatch, error mapping, and output/exit handling to `sdk/src/query/query-cli-adapter.ts` for better locality and testability.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3012
---
**Post-install message and update.md no longer recommend the removed `/gsd-reapply-patches` command** — after PR #2824 consolidated 86 skills into ~58, `/gsd-reapply-patches` was folded into a flag (`/gsd-update --reapply`). The 1.39.1 hotfix (#2954) updated `help.md` but missed `bin/install.js`'s `reportLocalPatches` runtime emitter, `get-shit-done/workflows/update.md` Step 4, and the English + zh-CN/ja-JP/ko-KR doc set. Users hit "Unknown command" after every install with backed-up patches. All five runtime branches in `reportLocalPatches` (claude, opencode, kilo, copilot, gemini, codex, cursor) now emit the consolidated form. Regression: `tests/bug-3010-reapply-patches-references.test.cjs` scans `bin/install.js`, every workflow file, and every doc (excluding CHANGELOG history and help.md's deprecation notice) for stale recommendations. See #3010.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 0
---
**Documentation refreshed for v1.40.0** — full audit of `docs/` against the 1.40.0-rc.1 release surface. Updates command lists, walkthroughs, and inventory rows for the 86→59 skill consolidation (#2790), the six namespace meta-skills with two-stage routing (#2792), the `/gsd-health --context` guard, the phase-lifecycle status-line read-side (#2833), and the Gemini colon-form / non-Gemini hyphen-form slash-command split. Translations in ja-JP/ko-KR/zh-CN/pt-BR mirror the structural changes; new English prose is marked with `<!-- TODO i18n -->` for human translator follow-up. CHANGELOG.md `[Unreleased]` section regrouped under Feature/Enhancement/Fix headers.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: TBD
---
**`dynamic_routing` block in `.planning/config.json` for failure-tier escalation (#3024).** Each agent declares a default tier (`light` / `standard` / `heavy`); when `dynamic_routing.enabled: true`, the resolver picks `tier_models[default_tier]` for the first spawn and escalates one tier up on orchestrator-detected soft failure (capped by `max_escalations`). Disabled by default — fully backward compatible. Composes with `model_overrides` (higher precedence) and `models.<phase_type>` (lower) for full cost-control flexibility. Adds new resolver `resolveModelForTier(cwd, agent, attempt)` to `core.cjs` for orchestrator integration.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3158
---
**SDK Runtime Bridge seam deepened** — dispatch is now centralized behind a native-first Runtime Bridge Module with explicit fallback policy (allowFallbackToSubprocess), strict native-only mode (strictSdk), and structured dispatch observability events; architecture/ADR docs updated to reflect the seam.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2975
---
**Changeset-fragment workflow** — eliminates CHANGELOG.md merge conflicts. Each PR drops `.changeset/<random-name>.md` with frontmatter (`type:`, `pr:`) plus a markdown body; the release-time `npm run changelog:render` consolidates fragments into `CHANGELOG.md` and deletes them. CI lint (`npm run lint:changeset`) requires a fragment on any PR touching user-facing files (`bin/`, `get-shit-done/`, `agents/`, `commands/`, `hooks/`, `sdk/src/`); contributors can opt out via the `no-changelog` label for purely internal changes. See [.changeset/README.md](.changeset/README.md) and CONTRIBUTING.md for the workflow.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3114
---
**`/gsd-progress --next` doc migration is fully consistent** — command docs now use clear `--next` wording, FEATURES TOC anchors match renamed headings, and regression tests enforce stale-command detection via structured slash-command token checks.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3117
---
**Worktree prune regression checks are now path-normalized** — pruning safety tests now parse `git worktree list --porcelain` and assert structured normalized paths, preventing path-separator false negatives across platforms while preserving non-destructive prune guarantees.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3119
---
**Optional findings probe guard checks now use structured parsing** — regression tests now parse fenced bash blocks and validate sketch/spike findings probes as structured command records, ensuring non-fatal `|| true` guards are enforced without raw source grep assertions.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3138
---
**`gsd-planner.md` directive language restored** — 10 instances of `CRITICAL`/`MANDATORY`/`ALWAYS`/`MUST` emphasis were silently removed in v1.38.4 (PR #2489) without documentation, conflicting with that release's stated sycophancy-hardening intent. Downstream effect: planner output in v1.38.4v1.40.x exhibited weaker adherence to user decisions and requirement coverage, as observed in #3087. Restored: `CRITICAL: User Decision Fidelity`, `CRITICAL: Never Simplify User Decisions`, `Multi-Source Coverage Audit (MANDATORY in every plan set)`, `Audit ALL four source types`, `Discovery is MANDATORY`, `ALWAYS split if:`, `requirements MUST list`, `CRITICAL: Every requirement ID MUST appear`, `ALWAYS use the Write tool`, and `CRITICAL — File naming convention`. Closes #3087.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3122
---
**Milestone close now repairs missing STATE narrative sections** — when `## Current Position` or `## Operator Next Steps` headings are absent, milestone completion appends canonical sections so state remains deterministic and consistently points operators to `/gsd-new-milestone`.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3111
---
**Progress routing command guidance remains canonical** — pre-planning assumption checks in progress routing now consistently assert and document `/gsd-discuss-phase` as the replacement path, with tests enforcing structured slash-command token checks.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3096
---
**`ai-integration-phase` Steps 7+8 now enforce sequential execution and Edit-only tool discipline** — when `gsd-ai-researcher` and `gsd-domain-researcher` were dispatched in parallel (an optimization an orchestrator could reasonably make since the sections appeared disjoint), `gsd-domain-researcher`'s `Write` call at finalization silently replaced the entire AI-SPEC.md with its pre-researcher copy, losing Sections 3/4. Confirmed at 40% incidence rate (2 of 5 agents on a real run). Fix adds an explicit sequential ordering note to Steps 7+8 ("MUST run sequentially — wait for Step 7 to complete before spawning Step 8") and injects Edit-only tool discipline into both agent prompts ("Use the Edit tool exclusively — NEVER use Write on this file"). Closes #3096.

View File

@@ -0,0 +1,11 @@
---
type: Fixed
pr: 3097
---
**Executor agents now detect and halt on cwd-drift out of worktrees (#3097)** — when a Bash call `cd`'d out of a worktree, `[ -f .git ]` became false (main repo's `.git` is a directory), silently skipping all HEAD/branch guards and allowing commits to land on the main repo's branch. Adds step 0a (cwd-drift sentinel using `git rev-parse --git-dir` + a per-worktree sentinel file at `.git/worktrees/<name>/gsd-spawn-toplevel`) to `gsd-executor.md`'s `task_commit_protocol`. Closes #3097.
---
type: Fixed
pr: 3099
---
**Executor agents now detect absolute paths that resolve outside the worktree (#3099)** — absolute paths constructed from the orchestrator's `pwd` (main repo root) resolved to the main repo when used in Edit/Write calls from a worktree, silently losing work. Adds step 0b (absolute-path guard using `WT_ROOT=$(git rev-parse --show-toplevel)`) with a clear warning and instructions to prefer relative paths. Both guards are documented in `references/worktree-path-safety.md` (loaded into every executor spawn prompt via `<execution_context>`). Closes #3099.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3142
---
**`secure-phase` no longer rubber-stamps SECURITY.md for legacy phases with no `<threat_model>` blocks** — Step 3's short-circuit previously exited to Step 6 (write clean SECURITY.md) whenever `threats_open: 0`, regardless of whether zero threats meant "all mitigated" or "none were ever written". Legacy phases authored before `<threat_model>` blocks became canonical now trigger **retroactive-STRIDE mode** in Step 5: the auditor builds a register from implementation files before verifying mitigations. Step 2c now tracks `register_authored_at_plan_time` and Step 3 gates the skip on both `threats_open: 0 AND register_authored_at_plan_time: true`. Closes #3120.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3121
---
**`gsd-sdk query commands` no longer returns "Unknown command"** — `commands` was referenced in `references/workstream-flag.md` and by agent tooling for verb discovery but had no SDK handler. A new `commandsList` handler in the native registry returns a sorted JSON array of all registered verb strings. `check.decision-coverage-plan` and `check.decision-coverage-verify` were already registered in the SDK native registry; the remaining gap was the `commands` introspection verb. Closes #3121.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3126
---
**`global:` skill resolution now uses the correct runtime home directory** — `buildAgentSkillsBlock()` hardcoded `globalSkillsBase` to `~/.claude/skills` regardless of the active runtime, causing every `global:` skill lookup to silently fail on non-Claude runtimes (Cursor, Gemini, Codex, Windsurf, etc.). Introduces `get-shit-done/bin/lib/runtime-homes.cjs` — a first-class runtime→directory mapping module covering all 15 supported runtimes with their canonical env-var overrides. Notable specifics: Hermes Agent uses a nested `skills/gsd/<skillName>/` layout (#2841); Cline is rules-based and returns `null` (no skills directory); `CLAUDE_CONFIG_DIR` env var was previously missing for Claude. Warning messages now show the actual runtime-specific path. Closes #3126.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3127
---
**`state.begin-phase` is now idempotent** — when called on a phase already in-flight (e.g. `--wave N` resume), it no longer overwrites `Current Plan`, `stopped_at` narrative, `Plan: N of M` body line, or `Last Activity Description` with stale values from the last `plan-phase` run. An idempotency guard reads the current `Status` field before writing: if it already contains `Executing Phase N`, only the `Last Activity` date and a resume-specific activity line are updated; all execution-progress fields are preserved. First-time execution (Status ≠ Executing) continues to write all fields as before. Closes #3127.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3128
---
**`roadmap.cjs` plan_count now correctly detects `{N}-PLAN-{NN}-{slug}.md` files** — the manager-dashboard plan-count filter matched only `*-PLAN.md` and `PLAN.md`, missing the slug-form layout (`5-PLAN-01-setup.md`) that `gsd-plan-phase` actually writes. `init manager` returned `plan_count: 0` / `disk_status: "discussed"` for fully-planned phases, causing the manager to recommend and dispatch redundant background planner agents. Same regex flaw as #2893 (fixed in `phase.cjs` via PR #2896); `roadmap.cjs` was missed in that sweep. Fix applies the same `looksLikePlanFile` logic (with `PLAN-OUTLINE` and `pre-bounce` exclusions) to `countPhasePlansAndSummaries`. Closes #3128.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3141
---
**`gsd-validate-commit.sh` community hook now catches all git commit forms** — the previous `[[ "$CMD" =~ ^git[[:space:]]+commit ]]` bash regex silently bypassed Conventional Commits enforcement for `git -C /path commit`, `GIT_AUTHOR_NAME=x git commit`, and `/usr/bin/git commit`. Introduces `hooks/lib/git-cmd.js` — a token-walk classifier (`isGitSubcommand(cmd, sub)`) that correctly handles env-prefix assignments, `-C path` working-directory flags, full-path executables, `--git-dir=` options, and all git global boolean flags. The hook now delegates detection to this module — the single source of truth for all hooks that gate on git subcommands. Closes #3129.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3130
---
**`update.md` npx invocations hardened against cache-stale and Bash-tool token-routing failures** — the previous `npx -y get-shit-done-cc@latest` form had two failure modes: (1) npx serving a cached older version instead of `@latest`, and (2) Bash-tool wrappers misrouting the `@` token, producing `Unknown command: "get-shit-done-cc@latest"`. All three sibling invocations (local, global, unknown/fallback) now use `npx -y --package=get-shit-done-cc@latest -- get-shit-done-cc` — the `--package=` flag forces a fresh registry fetch and the `--` separator prevents token misrouting. Closes #3130.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3135
---
**`/gsd-capture --backlog` now has a workflow to load** — PR #2824 consolidated `add-backlog` into the `--backlog` flag on `/gsd-capture` and wired `commands/gsd/capture.md` to delegate to `workflows/add-backlog.md` via `execution_context`. The workflow file was never created, leaving the routing with no implementation to load. Restores `get-shit-done/workflows/add-backlog.md` with the full process from the deleted `commands/gsd/add-backlog.md`: find next 999.x slot via `phase.next-decimal`, write ROADMAP entry before creating the phase directory (preserving the #2280 ordering invariant), create `.planning/phases/{N}-{slug}/`, and commit. Also fixes `docs/INVENTORY.md` which incorrectly attributed `--backlog` routing to `add-todo.md`. Adds a broad regression test that every `execution_context` `@`-reference in any `commands/gsd/*.md` resolves to an existing workflow file, preventing this class of gap from silently re-appearing. Closes #3135.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3155
---
**`stats.json` decimal phase ordering now has explicit regression coverage** — added a fixture ensuring `06.7/06.8/06.9` remain present when `06.10` exists, preventing dropped-phase regressions in mixed decimal phase ranges.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3153
---
**Statusline state rendering is now type-robust and YAML-list compatible** — milestone completion now renders for numeric and string `percent` values, and `next_phases` parsing supports both flow-array and block-list YAML forms.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3037
---
**Gemini local install no longer duplicates `/gsd:*` commands across user and workspace scopes** — when GSD is already installed at the user scope (`~/.gemini/commands/gsd/`) and you run `npx get-shit-done-cc --gemini --local` in a project, the installer now skips writing `commands/gsd/` to `<project>/.gemini/` and prints a one-line warning explaining why. Previously, both scopes received the same 65 command files, and Gemini's conflict detector renamed every `/gsd:*` command to `/workspace.gsd:*` and `/user.gsd:*`, breaking the documented namespace. Closes #3037.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2994
---
/gsd-reapply-patches Step 5 verifier now resolves at runtime — moved scripts/verify-reapply-patches.cjs to get-shit-done/bin/ which is shipped by the installer. The legacy scripts/ directory is not copied to user installs. See #2994.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3060
---
**Query mutation event mapping moved to dedicated module** — preserves event payloads while improving registry locality and test surface.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3026
---
**`gsd-sdk query <subcommand> --help` now reaches the handler instead of returning top-level usage.** The query argv parser harvested `--help` as a global flag and `main()` short-circuited dispatch — there was no path to discover what arguments a query subcommand accepts. The parser now leaves `--help` in `queryArgv` so the handler/fallback can render contextual help. The `gsd-tools.cjs` fallback now renders top-level usage on `--help` (instead of erroring), preserving #1818's anti-hallucination invariant by NOT executing the destructive command. See #3019.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3060
---
**Alias-family handler maps moved to dedicated catalog module** — keeps command keys/order while reducing createRegistry coupling and improving family-level locality.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3028
---
**Installer no longer prints `✓ GSD SDK ready` when the shim is unreachable from the user's runtime shells.** The previous check used `process.env.PATH` from the install subprocess, which often differs from the user's later interactive shells (POSIX `~/.local/bin` not in login shell, node-version-manager PATH shims). Added `getUserShellPath()` helper that probes `$SHELL -lc 'printf %s "$PATH"'` and `isGsdSdkOnPath(pathString?)` overload that accepts an explicit PATH; the install-time check now downgrades to the actionable `⚠` diagnostic from PR #3014 when install-PATH and user-shell-PATH disagree. Windows cross-shell support tracked separately. See #3020.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2840
---
**`docs/issue-driven-orchestration.md` — recipe for driving GSD from a tracker issue** — new guide that maps Symphony-style orchestration concepts (workflow, isolated agent workspace, proof-of-work, human review gate, follow-up capture) onto existing GSD primitives (`/gsd-new-workspace`, `/gsd-manager`, `/gsd-autonomous`, `/gsd-verify-work`, `/gsd-review`, `/gsd-ship`, `STATE.md`, phase artifacts). Documentation only — no new commands, no daemon, no tracker integration.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2994
---
/gsd-reapply-patches Step 5 verifier now resolves at runtime — moved scripts/verify-reapply-patches.cjs to get-shit-done/bin/ which is shipped by the installer. The legacy scripts/ directory is not copied to user installs. See #2994.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2979
---
Managed JS hooks now resolve under GUI/minimal-PATH runtimes — installer emits process.execPath (absolute, quoted, forward-slash-normalized) as the runner for every .js hook command instead of bare node. See #2979.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2995
---
Post-install path smoke test for workflow-invoked scripts — audits every node ${GSD_HOME}/...cjs invocation in workflows resolves at the runtime-installed path. See #2995.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3043
---
milestone complete now scopes phase stats to the explicit version argument and errors when that version is missing from a versioned ROADMAP milestone section.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3011
---
**Actionable diagnostic when `gsd-sdk` is not on PATH after install** — Windows users (and others on multi-shell setups) reported that the previous "GSD SDK files are present but `gsd-sdk` is not on your PATH" warning gave them no way to fix it: no path to look at, no shell-specific commands, no mention of the npx-cache caveat. New `formatSdkPathDiagnostic({ shimDir, platform, runDir })` helper returns a typed IR with the resolved shim location, platform-specific PATH-export commands (PowerShell / cmd.exe / Git Bash on Windows; `export PATH` on POSIX), and an npx-specific note when running under an `_npx` cache segment (where the shim may be written to a temp dir that won't persist). The console renderer in `bin/install.js` emits the lines from the IR; tests assert on the typed fields directly. (#3011)

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 3032
---
**Documentation: MCP tool schema as a context-budget concern (#3025).** Adds new sections to `get-shit-done/references/context-budget.md` and `docs/USER-GUIDE.md` explaining that every enabled MCP server injects its tool schema into every turn — heavyweight servers (browser/playwright, Mac-tools, Windows-tools) can cost 20k+ tokens each, often dwarfing what `model_profile` tuning saves. The toggle lives in `.claude/settings.json` (`enabledMcpjsonServers` / `disabledMcpjsonServers`) and is a Claude Code harness concern, not a GSD concern. Includes a pre-phase audit checklist (browser, platform-specific, cross-project, duplicates) and notes the multiplier interaction with `model_profile`. Companion to #3023 (per-phase-type model map) and #3024 (dynamic routing); together they cover the three biggest cost levers.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2997
---
SDK config-set/config-get and init responses no longer echo plaintext API keys. New sdk/src/query/secrets.ts ports SECRET_CONFIG_KEYS masking from CJS; init bundles only mask string values to preserve the boolean availability-flag contract. See #2997.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2992
---
/gsd-update queries wrong npm package names — moved package name into a deterministic check-latest-version.cjs script and updated the workflow to use ${GSD_DIR} from get_installed_version. See #2992.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3007
---
**PR templates now point at the changeset workflow** — the `Fix`, `Enhancement`, and `Feature` PR templates previously asked contributors to tick `CHANGELOG.md updated`, which contradicted the post-#2978 rule that `CHANGELOG.md` must not be edited directly. Each checkbox now references `npm run changeset` (and the `no-changelog` opt-out where applicable).

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3060
---
**CLI query CJS fallback execution extracted to dedicated adapter module** — preserves logs/help passthrough behavior while improving fallback locality and testability.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3060
---
**Query mutation event emission now uses a dedicated decorator seam** — preserves fire-and-forget behavior while reducing registry coupling and improving testability.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 3030
---
**`models` block in `.planning/config.json` for per-phase-type model selection (#3023).** A new resolution layer between per-agent `model_overrides` and the `model_profile` tier table. Six named slots (`planning` / `discuss` / `research` / `execution` / `verification` / `completion`) accept tier aliases (`opus` / `sonnet` / `haiku` / `inherit`). Lets you express "Opus for planning, Sonnet for the rest" in two lines without learning the agent taxonomy. Fully backward compatible — configs without `models` behave exactly as today.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2998
---
gsd-pristine/ is now populated by the installer when local patches are detected — saveLocalPatches calls a new populatePristineDir helper that runs the install transform pipeline into a tmp staging dir and copies modified files into pristineDir. The reapply-patches Step 5 verifier no longer falls back to its over-broad heuristic. See #2998.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2997
---
SDK config-set/config-get and init responses no longer echo plaintext API keys. New sdk/src/query/secrets.ts ports SECRET_CONFIG_KEYS masking from CJS; init bundles only mask string values to preserve the boolean availability-flag contract. See #2997.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2995
---
Post-install path smoke test for workflow-invoked scripts — audits every node ${GSD_HOME}/...cjs invocation in workflows resolves at the runtime-installed path. See #2995.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3108
---
Query module architecture deepened with compatibility-preserving seams — command policy now derives from command definitions, and dispatch/topology/registry seams are consolidated for better locality while preserving existing query behavior.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3112
---
Fixes for issue #3112 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3113
---
Fixes for issue #3113 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3115
---
Fixes for issue #3115 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3116
---
Fixes for issue #3116 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3118
---
Fixes for issue #3118 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3123
---
Fixes for issue #3123 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3124
---
Fixes for issue #3124 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3125
---
Fixes for issue #3098 were applied to keep command/workflow behavior and SDK parity aligned with current documented usage.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3060
---
**Query fallback orchestration now shared** — CLI and SDK query dispatch now use one planning seam for native vs CJS fallback decisions with behavior parity preserved.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3060
---
**Query/transport policy data now converged in shared module** — mutation and raw-output policy wiring now share one source of truth to reduce drift.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3042
---
**`/gsd-research-phase` consolidated into `/gsd-plan-phase --research-phase <N>`** — the standalone research command's slash-command stub was never registered (#3042). Rather than restore the orphan, the research-only capability now lives as a flag on `/gsd-plan-phase`. New modifiers: `--view` prints existing `RESEARCH.md` to stdout without spawning, `--research` forces refresh, otherwise prompts `update / view / skip` when `RESEARCH.md` already exists. Also scrubs four other stale slash-command references (`/gsd-check-todos`, `/gsd-new-workspace`, `/gsd-status`, residual `/gsd-plan-milestone-gaps`) across English + 4 localized doc sets (#3044). Closes #3042 and #3044.

View File

@@ -0,0 +1,6 @@
---
type: Changed
pr: 3131
---
**Re-wired 4 orphaned workflows as flags on parent commands** — six workflows were mis-categorised as "outright deleted dead skills" during the #2790 consolidation; two were caught by prior PRs (#3045, #3038) and four are fixed here. New flags: `/gsd-discuss-phase --assumptions` (surfaces Claude's implementation assumptions before planning), `/gsd-pause-work --report` (generates a post-session summary in `.planning/reports/`), `/gsd-manager --analyze-deps` (scans ROADMAP phases for dependency relationships before parallel execution), `/gsd-import --from-gsd2` (reverse-migrates a GSD-2 `.gsd/` project back to GSD v1 `.planning/` format). Also sweeps 29 stale `/gsd-*` command references across 27 user-facing files (English + 4 locales). Closes #3131.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 3029
---
**`/gsd-code-review-fix` and `/gsd-plan-milestone-gaps` no longer surface as "Unknown command"** — both were consolidated by #2790 (`/gsd-code-review --fix` and inline gap planning in `/gsd-audit-milestone` respectively), but several user-facing surfaces still emitted the old slash forms in their offer text. Fixed audit-milestone offer blocks, gsd-complete-milestone routing, code-review/execute-phase offer text, gsd-code-fixer agent role card, and the doc surfaces (USER-GUIDE, FEATURES, INVENTORY, AGENTS, CONFIGURATION). Closes #3029, closes #3034.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2990
---
gsd-code-fixer worktree no longer fails on the same-branch checkout — the agent now creates a new gsd-reviewfix/ branch via git worktree add -b and fast-forwards the user's branch on cleanup. See #2990.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2982
---
Extended no-source-grep lint to catch var-binding readFileSync.includes() pattern. Tests now fail when source-grep is hidden behind a parser wrapper. See #2982.

View File

@@ -0,0 +1,6 @@
---
type: Changed
pr: 3065
---
**Dispatch policy seam now returns a structured result contract** across native and fallback query execution paths (`ok`, typed error `kind`, `details`, and final `exit_code`), with CLI consuming the unified result instead of mixed throw/result handling.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3060
---
**Query static command registrations now split into domain catalog modules** — preserves command order/strings while improving registry locality and maintenance.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3173
---
**USER-GUIDE now documents installing for prerelease runtime editions** — adds a "Installing for Prerelease Editions (Next / Nightly / Insiders / Preview)" section with the `<RUNTIME>_CONFIG_DIR` env-var reference for every supported runtime. Resolves the discoverability gap behind requests like #3161 (Windsurf Next) without enumerating each prerelease channel as a separately tested runtime. Closes #3172.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 3085
---
**`GSDTools` query execution internals now use deep Module seams** — refactors runtime composition, native/subprocess adapters, and output projection behind stable public interfaces for better locality and testability.

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 2974
---
Migrated 8 test files from raw text matching (`stdout.includes(...)`, `assert.match(stderr, ...)`) to typed-IR assertions per CONTRIBUTING.md. Adds shared `ERROR_REASON` enum and `--json-errors` flag in `core.cjs`, typed `GRAPHIFY_REASON` in `graphify.cjs`, pure `buildSdkFailFastReport()` IR builder in `bin/install.js`, and Claude Code JSON envelope output (`hookSpecificOutput` with typed fields) for `gsd-session-state.sh` and `gsd-phase-boundary.sh`. Tests now assert on structured fields (`reason`, `context`, `state_present`, `planning_modified`, etc.) instead of substring matching. See #2974.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2795
---
**Optional update banner for non-GSD statusline users** — when the installer detects you've declined or kept a non-GSD statusline, it now offers an opt-in `SessionStart` banner that surfaces update availability via the existing `~/.cache/gsd/gsd-update-check.json` cache. Silent when up-to-date, rate-limits failure diagnostics to once per 24h, removed cleanly by `npx get-shit-done-cc --uninstall`.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2973
---
/gsd-profile-user --refresh writes dev-preferences.md to ~/.claude/skills/gsd-dev-preferences/SKILL.md instead of the legacy commands/gsd/ directory. Installer migrates any preserved legacy file to the new location. See #2973.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2992
---
/gsd-update queries wrong npm package names — moved package name into a deterministic check-latest-version.cjs script and updated the workflow to use ${GSD_DIR} from get_installed_version. See #2992.

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 2979
---
Managed JS hooks now resolve under GUI/minimal-PATH runtimes — installer emits process.execPath (absolute, quoted, forward-slash-normalized) as the runner for every .js hook command instead of bare node. See #2979.

View File

@@ -0,0 +1,5 @@
---
type: Added
pr: 2982
---
Extended no-source-grep lint to catch var-binding readFileSync.includes() pattern. Tests now fail when source-grep is hidden behind a parser wrapper. See #2982.

26
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,26 @@
# CodeRabbit configuration — gsd-build/get-shit-done
#
# Schema: https://docs.coderabbit.ai/reference/yaml-template/
#
# Project context: GSD ships a CLI tool + an agent runtime, not a documented
# public library. We carry rich JSDoc on internal helpers that warrant it
# (see bin/install.js, get-shit-done/bin/lib/*.cjs) but we do not enforce a
# blanket docstring coverage bar — see issue #2932 for rationale.
reviews:
pre_merge_checks:
# Disable docstring coverage check.
#
# The check produces false-positive warnings on PRs whose new code is
# entirely test files: it counts test(...) / beforeEach / afterEach
# arrow-function callbacks as functions and then reports 0% coverage
# because nothing has JSDoc. There is no per-check path filter in CR's
# documented schema that would let us exclude tests/** while keeping
# the check active elsewhere, and the top-level path_filters approach
# would silence ALL CR review on tests (security scans, out-of-scope
# checks, line-level findings) which we want to keep.
#
# All other CR pre-merge checks (out-of-scope, security, title) remain
# at their defaults.
docstrings:
mode: off

6
.githooks/pre-commit Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
if git diff --cached --name-only | grep -Eq "^sdk/src/query/command-manifest\.|^sdk/src/query/command-aliases\.generated\.ts$|^get-shit-done/bin/lib/command-aliases\.generated\.cjs$|^sdk/scripts/gen-command-aliases\.ts$"; then
npm run check:alias-drift
fi

48
.githooks/pre-push Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
zero_sha='0000000000000000000000000000000000000000'
blocked_regex="${GSD_BLOCKED_AUTHOR_REGEX:-}"
# Local-only guard: no-op unless the developer opts in via env var, e.g.
# export GSD_BLOCKED_AUTHOR_REGEX='@example-corp\.com$'
if [[ -z "$blocked_regex" ]]; then
exit 0
fi
violations=()
while read -r local_ref local_sha remote_ref remote_sha; do
# branch/tag deletion
if [[ "$local_sha" == "$zero_sha" ]]; then
continue
fi
if [[ "$remote_sha" == "$zero_sha" ]]; then
# New remote ref: inspect commits not already on any remote
commit_list=$(git rev-list "$local_sha" --not --remotes)
else
commit_list=$(git rev-list "$remote_sha..$local_sha")
fi
while read -r commit; do
[[ -z "$commit" ]] && continue
author_email=$(git show -s --format='%ae' "$commit")
lower_email=$(printf '%s' "$author_email" | tr '[:upper:]' '[:lower:]')
if printf '%s' "$lower_email" | grep -Eq "$blocked_regex"; then
violations+=("$commit <$author_email>")
fi
done <<< "$commit_list"
done
if [[ ${#violations[@]} -gt 0 ]]; then
{
echo "Push blocked: commit author email matched local blocked regex ($blocked_regex)."
echo "Rewrite author info before pushing these commits:"
for v in "${violations[@]}"; do
echo " - $v"
done
echo "Suggested fix: git rebase -i <base> --exec \"git commit --amend --no-edit --author='Your Name <non-enterprise@email>'\""
} >&2
exit 1
fi

View File

@@ -73,7 +73,7 @@ Closes #
- [ ] Changes are scoped to the approved enhancement — nothing extra included
- [ ] All existing tests pass (`npm test`)
- [ ] New or updated tests cover the enhanced behavior
- [ ] CHANGELOG.md updated
- [ ] `.changeset/` fragment added (`npm run changeset -- --type Changed --pr <NNN> --body "..."`) — or `no-changelog` label applied if not user-facing
- [ ] Documentation updated if behavior or output changed
- [ ] No unnecessary dependencies added

View File

@@ -94,7 +94,7 @@ Closes #
- [ ] Implementation scope matches the approved spec exactly
- [ ] All existing tests pass (`npm test`)
- [ ] New tests cover the happy path, error cases, and edge cases
- [ ] CHANGELOG.md updated with a user-facing description of the feature
- [ ] `.changeset/` fragment added with a user-facing description of the feature (`npm run changeset -- --type Added --pr <NNN> --body "..."`)
- [ ] Documentation updated — commands, workflows, references, README if applicable
- [ ] No unnecessary external dependencies added
- [ ] Works on Windows (backslash paths handled)

View File

@@ -63,7 +63,7 @@ Fixes #
- [ ] Fix is scoped to the reported bug — no unrelated changes included
- [ ] Regression test added (or explained why not)
- [ ] All existing tests pass (`npm test`)
- [ ] CHANGELOG.md updated if this is a user-facing fix
- [ ] `.changeset/` fragment added if this is a user-facing fix (`npm run changeset -- --type Fixed --pr <NNN> --body "..."`) — or `no-changelog` label applied
- [ ] No unnecessary dependencies added
## Breaking changes

View File

@@ -1,3 +1,12 @@
# Release stream policy:
# dev → @canary (this workflow — preview builds for the long-lived integration branch)
# main → @next (RC train, see release.yml)
# main → @latest (stable cuts, see release.yml)
#
# Streams do not mix. The publish/tag steps below gate on `refs/heads/dev` so a
# workflow_dispatch run on any other branch (including main) completes the
# build/test/dry-run validation but does not publish or tag.
name: Canary
on:
@@ -132,10 +141,14 @@ jobs:
env:
CANARY_VERSION: ${{ steps.canary.outputs.canary_version }}
DRY_RUN: ${{ inputs.dry_run }}
PUBLISH_ELIGIBLE: ${{ github.ref == 'refs/heads/dev' && !inputs.dry_run }}
BRANCH_REF: ${{ github.ref }}
run: |
echo "## Canary v${CANARY_VERSION}" >> "$GITHUB_STEP_SUMMARY"
if [ "$DRY_RUN" = "true" ]; then
echo "**DRY RUN** — npm publish, tagging, and push skipped" >> "$GITHUB_STEP_SUMMARY"
elif [ "$PUBLISH_ELIGIBLE" != "true" ]; then
echo "**VALIDATION ONLY** — publish/tag skipped for \`${BRANCH_REF}\`; canary publish is gated to \`refs/heads/dev\`." >> "$GITHUB_STEP_SUMMARY"
else
echo "- Published to npm as \`canary\`" >> "$GITHUB_STEP_SUMMARY"
echo "- SDK also published: \`@gsd-build/sdk@${CANARY_VERSION}\` on \`canary\`" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -0,0 +1,24 @@
name: Changeset Required
on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]
permissions:
contents: read
pull-requests: read
jobs:
changeset-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '24'
- name: Run changeset lint
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: node scripts/changeset/lint.cjs

View File

@@ -1,5 +1,27 @@
name: Hotfix Release
# Hotfix flow for X.YY.Z patch releases (Z > 0).
#
# create:
# - Branches hotfix/X.YY.Z from the highest existing vX.YY.* tag (1.27.2 from
# v1.27.1, 1.27.1 from v1.27.0). The base IS the cumulative-fix anchor for
# the previous patch.
# - Auto-cherry-picks every fix:/chore: commit on origin/main that isn't
# already in the base, oldest-first. Patch-equivalents (already applied)
# are skipped via `git cherry`. feat:/refactor: are NEVER auto-included.
# - Conflicts fail the workflow with the offending SHA so the operator can
# resolve manually on the branch and re-run finalize with auto_cherry_pick=false.
# - Step summary lists every included SHA so the eventual vX.YY.Z tag
# self-documents what shipped.
#
# finalize:
# - install-smoke gate (cross-platform, parity with release.yml/release-sdk.yml)
# - Bundles SDK as both loose tree (sdk/dist/cli.js) and recoverable tarball
# (sdk-bundle/gsd-sdk.tgz) — parity with release-sdk.yml so a hotfix shipped
# during the @gsd-build-token outage carries the same payload shape.
# - Publishes to @latest, tags vX.YY.Z, re-points @next → vX.YY.Z, opens
# merge-back PR.
on:
workflow_dispatch:
inputs:
@@ -14,6 +36,11 @@ on:
description: 'Patch version (e.g., 1.27.1)'
required: true
type: string
auto_cherry_pick:
description: 'Auto-cherry-pick fix:/chore: commits from origin/main since base tag (create only)'
required: false
type: boolean
default: true
dry_run:
description: 'Dry run (skip npm publish, tagging, and push)'
required: false
@@ -54,10 +81,13 @@ jobs:
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1-2)
TARGET_TAG="v${VERSION}"
BRANCH="hotfix/${VERSION}"
BASE_TAG=$(git tag -l "v${MAJOR_MINOR}.*" \
| grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$" \
# Append TARGET_TAG to the candidate list, then sort -V, then walk the
# sorted list and print whatever immediately precedes TARGET_TAG. This
# is semver-correct for multi-digit patches (v1.27.10 > v1.27.9) where
# a plain `awk '$1 < target'` lexicographic compare would mis-order.
BASE_TAG=$( ( git tag -l "v${MAJOR_MINOR}.*" | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$"; echo "$TARGET_TAG" ) \
| sort -V \
| awk -v target="$TARGET_TAG" '$1 < target { last=$1 } END { if (last != "") print last }')
| awk -v target="$TARGET_TAG" '$1 == target { print prev; exit } { prev = $1 }')
if [ -z "$BASE_TAG" ]; then
echo "::error::No prior stable tag found for ${MAJOR_MINOR}.x before $TARGET_TAG"
exit 1
@@ -95,29 +125,160 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Create hotfix branch
if: inputs.dry_run != 'true'
- name: Create hotfix branch from base tag and push (skeleton)
env:
BRANCH: ${{ needs.validate-version.outputs.branch }}
BASE_TAG: ${{ needs.validate-version.outputs.base_tag }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
git checkout -b "$BRANCH" "$BASE_TAG"
# Push the skeleton branch up-front so any subsequent cherry-pick
# conflict leaves a remote artefact the operator can fetch, resolve,
# and re-push. Skipped on dry-run — local checkout still exercises
# the same cherry-pick + bump flow so conflicts are caught.
if [ "$DRY_RUN" != "true" ]; then
git push -u origin "$BRANCH"
fi
- name: Cherry-pick fix/chore commits from origin/main since base tag
if: ${{ inputs.auto_cherry_pick }}
env:
BRANCH: ${{ needs.validate-version.outputs.branch }}
BASE_TAG: ${{ needs.validate-version.outputs.base_tag }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
git fetch origin main:refs/remotes/origin/main
# `git cherry $BASE_TAG origin/main` lists every commit on main not
# patch-equivalent in BASE_TAG. + means needs picking, - means
# already applied (skipped silently).
CANDIDATES=$(git cherry "$BASE_TAG" origin/main | awk '/^\+ / {print $2}')
if [ -z "$CANDIDATES" ]; then
echo "No commits on origin/main beyond $BASE_TAG."
echo "## Cherry-pick summary" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Base: \`$BASE_TAG\` — no commits to consider." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
# Re-order chronologically (oldest first) for predictable application.
ORDERED=$(git log --reverse --format='%H' "$BASE_TAG..origin/main" \
| grep -F -f <(echo "$CANDIDATES") || true)
INCLUDED=""
SKIPPED=""
while IFS= read -r SHA; do
[ -z "$SHA" ] && continue
SUBJECT=$(git log -1 --format='%s' "$SHA")
# fix: or chore:, optional scope, optional ! breaking marker
if echo "$SUBJECT" | grep -qE '^(fix|chore)(\([^)]+\))?!?: '; then
echo "→ cherry-picking $SHA $SUBJECT"
if ! git cherry-pick -x "$SHA"; then
# Abort restores HEAD to the last successful pick. On real
# runs, push that state so the operator can fetch, resolve
# $SHA manually, and finalize with auto_cherry_pick=false.
git cherry-pick --abort || true
if [ "$DRY_RUN" != "true" ]; then
git push --force-with-lease origin "$BRANCH" || git push origin "$BRANCH" || true
fi
{
echo "## Cherry-pick conflict"
echo ""
echo "Failed at: \`${SHA}\` — \`${SUBJECT}\`"
echo ""
if [ "$DRY_RUN" = "true" ]; then
echo "**Dry run:** branch was not pushed, so the picks below were discarded with the runner."
if [ -n "$INCLUDED" ]; then
echo ""
echo "Already-applied picks (lost — must be re-applied before resolving \`${SHA}\`):"
echo ""
echo "$INCLUDED"
fi
echo ""
echo "**To resolve:** re-run \`create\` with \`auto_cherry_pick=true\` (real, not dry-run) to materialize the partial branch on origin, then resolve \`${SHA}\` manually. Re-running with \`auto_cherry_pick=false\` would recreate the branch from \`${BASE_TAG}\` and lose every pick listed above."
else
echo "Branch \`${BRANCH}\` was pushed with picks applied up to (but not including) the conflicting commit."
echo ""
echo "**To resolve:** \`git fetch origin && git checkout ${BRANCH} && git cherry-pick -x ${SHA}\`, fix the conflict, push, then re-run \`finalize\` with \`auto_cherry_pick=false\`."
fi
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::Cherry-pick of $SHA failed. See summary."
exit 1
fi
INCLUDED="${INCLUDED}- \`${SHA}\` ${SUBJECT}"$'\n'
else
echo " skip $SHA $SUBJECT (not fix/chore)"
SKIPPED="${SKIPPED}- \`${SHA}\` ${SUBJECT}"$'\n'
fi
done <<< "$ORDERED"
{
echo "## Cherry-pick summary"
echo ""
echo "Base: \`$BASE_TAG\`"
echo ""
if [ -n "$INCLUDED" ]; then
echo "### Included (fix/chore)"
echo ""
echo "$INCLUDED"
else
echo "_No fix/chore commits to include._"
echo ""
fi
if [ -n "$SKIPPED" ]; then
echo "### Skipped (feat/refactor/etc — not auto-included)"
echo ""
echo "$SKIPPED"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Bump version and push
env:
BRANCH: ${{ needs.validate-version.outputs.branch }}
BASE_TAG: ${{ needs.validate-version.outputs.base_tag }}
VERSION: ${{ inputs.version }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
git checkout -b "$BRANCH" "$BASE_TAG"
# Bump version in package.json
set -euo pipefail
npm version "$VERSION" --no-git-tag-version
git add package.json package-lock.json
# Keep sdk/package.json in lockstep (parity with release-sdk.yml).
if [ -f sdk/package.json ]; then
(cd sdk && npm version "$VERSION" --no-git-tag-version)
git add sdk/package.json
[ -f sdk/package-lock.json ] && git add sdk/package-lock.json
fi
git commit -m "chore: bump version to $VERSION for hotfix"
git push origin "$BRANCH"
echo "## Hotfix branch created" >> "$GITHUB_STEP_SUMMARY"
echo "- Branch: \`$BRANCH\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Based on: \`$BASE_TAG\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Apply your fix, push, then run this workflow again with \`finalize\`" >> "$GITHUB_STEP_SUMMARY"
if [ "$DRY_RUN" != "true" ]; then
git push origin "$BRANCH"
else
echo "DRY RUN — branch not pushed. Local checkout exercised the cherry-pick and bump flow."
fi
{
echo "## Hotfix branch created"
echo ""
echo "- Branch: \`$BRANCH\`"
echo "- Based on: \`$BASE_TAG\`"
echo "- Apply additional manual fixes if needed, then run \`finalize\`."
} >> "$GITHUB_STEP_SUMMARY"
finalize:
install-smoke:
needs: validate-version
if: inputs.action == 'finalize'
permissions:
contents: read
uses: ./.github/workflows/install-smoke.yml
with:
ref: ${{ needs.validate-version.outputs.branch }}
finalize:
needs: [validate-version, install-smoke]
if: inputs.action == 'finalize'
runs-on: ubuntu-latest
timeout-minutes: 10
timeout-minutes: 15
permissions:
contents: write
pull-requests: write
@@ -140,31 +301,83 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Detect prior publish (reconciliation mode)
id: prior_publish
env:
VERSION: ${{ inputs.version }}
run: |
EXISTING=$(npm view get-shit-done-cc@"$VERSION" version 2>/dev/null || true)
if [ -n "$EXISTING" ]; then
echo "::warning::get-shit-done-cc@${VERSION} is already on the registry — entering reconciliation mode (skip publish, continue with tag/release/PR/dist-tag)."
echo "skip_publish=true" >> "$GITHUB_OUTPUT"
else
echo "skip_publish=false" >> "$GITHUB_OUTPUT"
fi
- name: Install and test
run: |
npm ci
npm run test:coverage
- name: Create PR to merge hotfix back to main
if: ${{ !inputs.dry_run }}
- name: Build SDK dist for tarball
run: npm run build:sdk
- name: Verify CC tarball ships sdk/dist/cli.js (bug #2647 guard)
run: bash scripts/verify-tarball-sdk-dist.sh
- name: Pack SDK as tarball and bundle into CC source tree
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ needs.validate-version.outputs.branch }}
VERSION: ${{ inputs.version }}
run: |
EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number')
if [ -n "$EXISTING_PR" ]; then
echo "PR #$EXISTING_PR already exists; updating"
gh pr edit "$EXISTING_PR" \
--title "chore: merge hotfix v${VERSION} back to main" \
--body "Merge hotfix changes back to main after v${VERSION} release."
else
gh pr create \
--base main \
--head "$BRANCH" \
--title "chore: merge hotfix v${VERSION} back to main" \
--body "Merge hotfix changes back to main after v${VERSION} release."
set -e
cd sdk
npm pack
TARBALL="gsd-build-sdk-${VERSION}.tgz"
if [ ! -f "$TARBALL" ]; then
echo "::error::Expected $TARBALL but npm pack did not produce it."
ls -la
exit 1
fi
mkdir -p ../sdk-bundle
mv "$TARBALL" ../sdk-bundle/gsd-sdk.tgz
cd ..
ls -la sdk-bundle/
- name: Add sdk-bundle to CC files whitelist (in-tree, not committed)
run: |
node <<'NODE'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
if (!Array.isArray(pkg.files)) {
console.error('::error::package.json files is not an array');
process.exit(1);
}
if (!pkg.files.includes('sdk-bundle')) {
pkg.files.push('sdk-bundle');
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
console.log('Added sdk-bundle/ to package.json files whitelist');
}
NODE
- name: Verify CC tarball will contain sdk-bundle/gsd-sdk.tgz
run: |
set -e
TARBALL=$(npm pack --ignore-scripts 2>/dev/null | tail -1)
if [ -z "$TARBALL" ] || [ ! -f "$TARBALL" ]; then
echo "::error::npm pack produced no tarball"
exit 1
fi
if ! tar -tzf "$TARBALL" | grep -q "package/sdk-bundle/gsd-sdk.tgz"; then
echo "::error::CC tarball is missing package/sdk-bundle/gsd-sdk.tgz"
exit 1
fi
echo "✅ CC tarball contains sdk-bundle/gsd-sdk.tgz"
rm -f "$TARBALL"
- name: Dry-run publish validation
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --dry-run --tag latest
- name: Tag and push
if: ${{ !inputs.dry_run }}
@@ -185,55 +398,98 @@ jobs:
fi
- name: Publish to npm (latest)
if: ${{ !inputs.dry_run }}
run: npm publish --provenance --access public
if: ${{ !inputs.dry_run && steps.prior_publish.outputs.skip_publish != 'true' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm publish --provenance --access public --tag latest
- name: Create GitHub Release
- name: Re-point next dist-tag at this hotfix
if: ${{ !inputs.dry_run }}
env:
VERSION: ${{ inputs.version }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm dist-tag add "get-shit-done-cc@${VERSION}" next
echo "✅ next dist-tag re-pointed to v${VERSION} (matches latest)"
- name: Create GitHub Release (idempotent)
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
run: |
gh release create "v${VERSION}" \
--title "v${VERSION} (hotfix)" \
--generate-notes
if gh release view "v${VERSION}" >/dev/null 2>&1; then
echo "GitHub Release v${VERSION} already exists; ensuring --latest flag is set"
gh release edit "v${VERSION}" --latest || true
else
gh release create "v${VERSION}" \
--title "v${VERSION} (hotfix)" \
--generate-notes \
--latest
fi
- name: Clean up next dist-tag
- name: Create PR to merge hotfix back to main
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ needs.validate-version.outputs.branch }}
VERSION: ${{ inputs.version }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
# Point next to the stable release so @next never returns something
# older than @latest. This prevents stale pre-release installs.
npm dist-tag add "get-shit-done-cc@${VERSION}" next 2>/dev/null || true
echo "✓ next dist-tag updated to v${VERSION}"
EXISTING_PR=$(gh pr list --base main --head "$BRANCH" --state open --json number --jq '.[0].number')
if [ -n "$EXISTING_PR" ]; then
gh pr edit "$EXISTING_PR" \
--title "chore: merge hotfix v${VERSION} back to main" \
--body "Merge hotfix changes back to main after v${VERSION} release."
else
gh pr create \
--base main \
--head "$BRANCH" \
--title "chore: merge hotfix v${VERSION} back to main" \
--body "Merge hotfix changes back to main after v${VERSION} release."
fi
- name: Verify publish
- name: Verify publish landed on registry
if: ${{ !inputs.dry_run }}
env:
VERSION: ${{ inputs.version }}
run: |
sleep 10
PUBLISHED=$(npm view get-shit-done-cc@"$VERSION" version 2>/dev/null || echo "NOT_FOUND")
PUBLISHED="NOT_FOUND"
for delay in 5 10 20 30 45; do
PUBLISHED=$(npm view get-shit-done-cc@"$VERSION" version 2>/dev/null || echo "NOT_FOUND")
if [ "$PUBLISHED" = "$VERSION" ]; then
break
fi
echo "Waiting ${delay}s for registry to catch up (saw: $PUBLISHED)..."
sleep "$delay"
done
if [ "$PUBLISHED" != "$VERSION" ]; then
echo "::error::Published version verification failed. Expected $VERSION, got $PUBLISHED"
echo "::error::Version $VERSION did not appear on the registry within timeout"
exit 1
fi
echo "✓ Verified: get-shit-done-cc@$VERSION is live on npm"
LATEST_VER=$(npm view get-shit-done-cc dist-tags.latest 2>/dev/null || echo "NOT_FOUND")
if [ "$LATEST_VER" != "$VERSION" ]; then
echo "::error::dist-tag 'latest' resolves to '$LATEST_VER', expected '$VERSION'"
exit 1
fi
echo "✓ Verified: get-shit-done-cc@$VERSION is live on @latest"
- name: Summary
env:
VERSION: ${{ inputs.version }}
BASE_TAG: ${{ needs.validate-version.outputs.base_tag }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
echo "## Hotfix v${VERSION}" >> "$GITHUB_STEP_SUMMARY"
if [ "$DRY_RUN" = "true" ]; then
echo "**DRY RUN** — npm publish, tagging, and push skipped" >> "$GITHUB_STEP_SUMMARY"
else
echo "- Published to npm as \`latest\`" >> "$GITHUB_STEP_SUMMARY"
echo "- Tagged \`v${VERSION}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- PR created to merge back to main" >> "$GITHUB_STEP_SUMMARY"
fi
{
echo "## Hotfix v${VERSION}"
echo ""
echo "- Base (cumulative-fix anchor): \`${BASE_TAG}\`"
if [ "$DRY_RUN" = "true" ]; then
echo "- **DRY RUN** — npm publish, tagging, and push skipped"
else
echo "- Published to npm as \`latest\`"
echo "- \`next\` dist-tag re-pointed to v${VERSION}"
echo "- Tagged \`v${VERSION}\` (anchor for the next hotfix's cherry-pick base)"
echo "- SDK bundled at \`sdk-bundle/gsd-sdk.tgz\` inside CC tarball"
echo "- Merge-back PR opened against main"
fi
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -25,61 +25,430 @@ name: Release SDK Bundle
on:
workflow_dispatch:
inputs:
tag:
description: 'npm dist-tag to publish under'
action:
description: 'publish = normal dev/next/latest publish; hotfix = create hotfix/X.YY.Z branch from latest vX.YY.* tag, cherry-pick fix:/chore: from main, publish to @latest'
required: true
type: choice
default: publish
options:
- publish
- hotfix
tag:
description: 'npm dist-tag (publish action only; hotfix forces latest)'
required: false
type: choice
default: latest
options:
- dev
- next
- latest
version:
description: 'Explicit version (e.g. 1.50.0-dev.3, 1.50.0-rc.2, 1.50.0). Empty = derive from package.json base + tag-appropriate suffix.'
description: 'Version. publish: explicit (e.g. 1.50.0-dev.3) or empty to derive. hotfix: REQUIRED patch (e.g. 1.27.1, Z>0).'
required: false
type: string
ref:
description: 'Branch or ref to build from (default: the workflow-dispatch ref, typically dev)'
description: 'Branch or ref to build from. Ignored for hotfix (workflow uses hotfix/X.YY.Z).'
required: false
type: string
auto_cherry_pick:
description: 'Hotfix only: auto-cherry-pick fix:/chore: commits from origin/main since base tag.'
required: false
type: boolean
default: true
dry_run:
description: 'Dry run (skip npm publish, git tag, and push)'
description: 'Dry run (skip npm publish, git tag, and push). Hotfix branch creation/push also skipped.'
required: false
type: boolean
default: false
# Per dist-tag, no concurrent publishes for the same stream. Different streams
# can publish in parallel because they target different dist-tags.
# Per stream (dist-tag for publish, version for hotfix) — no concurrent publishes for the same stream.
concurrency:
group: release-sdk-${{ inputs.tag }}
group: release-sdk-${{ inputs.action == 'hotfix' && format('hotfix-{0}', inputs.version) || inputs.tag }}
cancel-in-progress: false
env:
NODE_VERSION: 24
jobs:
# Resolves the effective git ref for this run.
#
# action=publish → outputs inputs.ref verbatim (may be empty = workflow ref)
# action=hotfix → branches hotfix/X.YY.Z from highest existing vX.YY.* tag,
# auto-cherry-picks fix:/chore: from origin/main, pushes,
# and outputs the new branch as ref. Idempotent: if branch
# already exists (operator pre-prepared it via hotfix.yml),
# we just check it out and re-run the cherry-pick step
# no-ops since `git cherry` will report nothing new.
prepare:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
outputs:
ref: ${{ steps.out.outputs.ref }}
base_tag: ${{ steps.hotfix.outputs.base_tag }}
steps:
- name: Validate hotfix inputs
if: inputs.action == 'hotfix'
env:
VERSION: ${{ inputs.version }}
run: |
if [ -z "$VERSION" ]; then
echo "::error::action=hotfix requires the 'version' input (e.g. 1.27.1)"
exit 1
fi
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[1-9][0-9]*$'; then
echo "::error::Hotfix version must match X.YY.Z with Z>0 (got: $VERSION)"
exit 1
fi
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
if: inputs.action == 'hotfix'
with:
fetch-depth: 0
- name: Configure git identity
if: inputs.action == 'hotfix'
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Prepare hotfix branch
id: hotfix
if: inputs.action == 'hotfix'
env:
VERSION: ${{ inputs.version }}
AUTO_CHERRY_PICK: ${{ inputs.auto_cherry_pick }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
set -euo pipefail
# Stash the shipped-paths classifier from the dispatched ref's
# working tree BEFORE `git checkout -b ... "$BASE_TAG"` below
# overwrites it. Base tags predating #2980 don't have the
# classifier in their tree, so the loop must reference a
# location that survives the working-tree swap. Bug #2983.
CLASSIFIER_SRC="scripts/diff-touches-shipped-paths.cjs"
if [ ! -f "$CLASSIFIER_SRC" ]; then
echo "::error::shipped-paths classifier not found at $CLASSIFIER_SRC in dispatched ref — refusing to run"
exit 1
fi
CLASSIFIER="${RUNNER_TEMP}/diff-touches-shipped-paths.cjs"
cp "$CLASSIFIER_SRC" "$CLASSIFIER"
if [ ! -f "$CLASSIFIER" ]; then
echo "::error::failed to stage classifier at $CLASSIFIER"
exit 1
fi
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1-2)
TARGET_TAG="v${VERSION}"
BRANCH="hotfix/${VERSION}"
# Semver-correct selection: append TARGET_TAG, sort -V, take preceding entry.
# Plain lexicographic compare mis-orders multi-digit patches (v1.27.10 vs v1.27.9).
BASE_TAG=$( ( git tag -l "v${MAJOR_MINOR}.*" | grep -E "^v[0-9]+\.[0-9]+\.[0-9]+$"; echo "$TARGET_TAG" ) \
| sort -V \
| awk -v target="$TARGET_TAG" '$1 == target { print prev; exit } { prev = $1 }')
if [ -z "$BASE_TAG" ]; then
echo "::error::No prior stable tag found for ${MAJOR_MINOR}.x before $TARGET_TAG"
exit 1
fi
echo "base_tag=$BASE_TAG" >> "$GITHUB_OUTPUT"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
# Idempotent branch creation — operator may have pre-prepared via hotfix.yml.
git fetch origin main:refs/remotes/origin/main
if git ls-remote --exit-code origin "refs/heads/$BRANCH" >/dev/null 2>&1; then
echo "Branch $BRANCH already exists on origin; checking out"
git fetch origin "$BRANCH"
git checkout "$BRANCH"
BRANCH_PRE_EXISTED=1
else
git checkout -b "$BRANCH" "$BASE_TAG"
BRANCH_PRE_EXISTED=0
# Push the skeleton up-front (real runs only) so cherry-pick conflicts
# leave a remote artefact the operator can resolve. Dry-run keeps
# everything local — no orphan branch created on origin.
if [ "$DRY_RUN" != "true" ]; then
git push -u origin "$BRANCH"
fi
fi
if [ "$AUTO_CHERRY_PICK" = "true" ]; then
CANDIDATES=$(git cherry HEAD origin/main | awk '/^\+ / {print $2}')
if [ -n "$CANDIDATES" ]; then
ORDERED=$(git log --reverse --format='%H' "${BASE_TAG}..origin/main" \
| grep -F -f <(echo "$CANDIDATES") || true)
INCLUDED=""
# POLICY_SKIPPED — commits intentionally not picked because they
# don't match the fix/chore filter (feat/refactor/docs/etc).
# CONFLICT_SKIPPED — fix/chore commits whose cherry-pick failed
# and were skipped per the full-automation policy (#2968).
# NON_SHIPPED_SKIPPED — fix/chore commits whose diff doesn't
# touch any path in the npm tarball's `files` whitelist
# (CI / test / docs / planning-only changes). They can't
# affect the published package's behavior, so picking them
# into a hotfix is meaningless — and picking workflow-file
# changes specifically would also fail the push step because
# the default GITHUB_TOKEN lacks the `workflow` scope. The
# shipped-paths filter is the precise root cause: bug #2980.
# Operators reviewing the run summary need these distinct so
# the manual-review queue (CONFLICT_SKIPPED) isn't buried in
# the noise from the other two buckets.
POLICY_SKIPPED=""
CONFLICT_SKIPPED=""
NON_SHIPPED_SKIPPED=""
while IFS= read -r SHA; do
[ -z "$SHA" ] && continue
SUBJECT=$(git log -1 --format='%s' "$SHA")
if echo "$SUBJECT" | grep -qE '^(fix|chore)(\([^)]+\))?!?: '; then
# Merge commits with fix:/chore: titles can't be cherry-picked
# without `-m <parent>` and we can't pick the parent
# automatically. They fail BEFORE entering cherry-pick state
# (no CHERRY_PICK_HEAD), so an unconditional `--skip` would
# then fail and brick the loop. Skip them upfront with a
# distinct reason. Bug #2968 / CodeRabbit on PR #2970.
PARENT_COUNT=$(git rev-list --parents -n 1 "$SHA" | awk '{print NF - 1}')
if [ "$PARENT_COUNT" -gt 1 ]; then
REASON="merge commit — manual -m parent selection required"
echo "↷ skipping $SHA — $REASON"
CONFLICT_SKIPPED="${CONFLICT_SKIPPED}- \`${SHA}\` ${SUBJECT} ($REASON)"$'\n'
continue
fi
# Pre-pick guard: a hotfix release can only be affected
# by commits whose diff intersects the npm tarball's
# shipped paths (package.json `files` whitelist plus
# package.json itself, which `npm pack` always
# includes). Commits that touch only CI workflows,
# tests, docs, or planning artifacts cannot change what
# ships, so picking them into a hotfix is meaningless.
# As a side benefit, this excludes
# `.github/workflows/*` changes whose push would
# otherwise be rejected by GitHub because the default
# GITHUB_TOKEN lacks the `workflow` scope. The filter
# is implemented in
# scripts/diff-touches-shipped-paths.cjs rather than
# inline so the rules (read package.json `files`,
# treat entries as file-OR-directory prefix, the
# `package.json`-always-shipped rule) are
# unit-testable. Bug #2980.
#
# Use $CLASSIFIER (staged at workflow-start, before
# `git checkout -b ... "$BASE_TAG"` swapped the working
# tree) rather than `scripts/...` directly — base tags
# older than #2980 don't have the classifier in their
# tree. Capture the exit code via PIPESTATUS and
# dispatch on it: 0 = shipped, 1 = not shipped, 2+ =
# classifier error → fail-fast (don't silently treat
# tooling errors as informational skips). Bug #2983.
#
# PIPESTATUS capture must happen IMMEDIATELY after the
# pipeline — the previous form (`pipeline || true; RC=
# ${PIPESTATUS[1]}`) had a subtle bug: when the
# pipeline fails (exit 1 or 2 — exactly the cases we
# care about), `|| true` runs `true` as a one-command
# pipeline, overwriting PIPESTATUS to (0). The fix is
# to wrap the pipeline in `set +e`/`set -e` and snapshot
# PIPESTATUS into a local array on the very next line.
# CodeRabbit on PR #2984.
set +e
git diff-tree --no-commit-id --name-only -r "$SHA" \
| node "$CLASSIFIER"
PIPE_RC=("${PIPESTATUS[@]}")
set -e
DIFFTREE_RC="${PIPE_RC[0]}"
CLASSIFIER_RC="${PIPE_RC[1]}"
if [ "$DIFFTREE_RC" -ne 0 ]; then
echo "::error::git diff-tree failed for $SHA (exit $DIFFTREE_RC) — refusing to classify on incomplete input."
exit "$DIFFTREE_RC"
fi
case "$CLASSIFIER_RC" in
0) ;;
1)
REASON="touches no shipped paths (CI / test / docs / planning only)"
echo "↷ skipping $SHA — $REASON"
NON_SHIPPED_SKIPPED="${NON_SHIPPED_SKIPPED}- \`${SHA}\` ${SUBJECT}"$'\n'
continue
;;
*)
echo "::error::shipped-paths classifier failed for $SHA (exit $CLASSIFIER_RC). Refusing to silently skip — bug #2983."
exit "$CLASSIFIER_RC"
;;
esac
echo "→ cherry-picking $SHA $SUBJECT"
# Pin merge.conflictStyle=merge on the cherry-pick so the
# awk classifier below sees deterministic marker shapes —
# diff3/zdiff3 would inject `||||||| ancestor` lines into
# the HEAD section and cause context-missing conflicts to
# misclassify as real. Bug #2966.
if ! git -c merge.conflictStyle=merge cherry-pick -x --allow-empty --keep-redundant-commits "$SHA"; then
# Full automation policy (bug #2968): any conflict the
# cherry-pick can't auto-resolve is skipped, not aborted.
# The hotfix run completes with whatever applies cleanly;
# the CONFLICT_SKIPPED list below becomes the operator's
# review queue (see "Cherry-pick summary" in the run
# summary).
#
# Classify the conflict for the skip reason (operator-
# facing diagnostic — doesn't change control flow):
# - context absent at base: HEAD section in every
# conflict marker is empty (the picked commit modifies
# code that doesn't exist at the base). Bug #2966.
# - merge conflict: HEAD section has content (both base
# and patch want different content for the same
# region). Typical when the base tag was cut from a
# branch that has diverged from main. Bug #2968.
UNMERGED=$(git diff --name-only --diff-filter=U)
REASON="merge conflict — manual review"
if [ -n "$UNMERGED" ]; then
ALL_EMPTY_HEAD=true
while IFS= read -r CONFLICTED; do
[ -z "$CONFLICTED" ] && continue
# Guard the classifier against degenerate cases that
# would otherwise skew toward "context absent" (the
# auto-skip path) when they're actually unsafe to skip:
# - file missing or unreadable: don't pretend the
# conflict is benign; treat as real.
# - file listed as unmerged but no conflict markers
# present: anomalous git state; treat as real so
# the pick goes to the manual-review queue.
# CodeRabbit on PR #2970.
if [ ! -r "$CONFLICTED" ] || ! grep -q '^<<<<<<< ' "$CONFLICTED" 2>/dev/null; then
ALL_EMPTY_HEAD=false
break
fi
REAL=$(awk '
/^<<<<<<< / { in_head=1; head=""; next }
/^=======$/ && in_head { in_head=0; next }
/^>>>>>>> / {
if (head ~ /[^[:space:]]/) { print "real"; exit }
head=""
next
}
in_head { head = head $0 "\n" }
' "$CONFLICTED" 2>/dev/null || echo "real")
if [ "$REAL" = "real" ]; then
ALL_EMPTY_HEAD=false
break
fi
done <<< "$UNMERGED"
if [ "$ALL_EMPTY_HEAD" = "true" ]; then
REASON="context absent at base"
fi
fi
echo "↷ skipping $SHA — $REASON"
# Guard `--skip`: cherry-pick can fail before entering the
# conflict state (e.g. unreadable commit, empty-without-
# --allow-empty edge cases the flag misses). Calling
# `--skip` outside an in-progress cherry-pick exits non-
# zero and would brick the loop. CodeRabbit on PR #2970.
if git rev-parse -q --verify CHERRY_PICK_HEAD >/dev/null 2>&1; then
git cherry-pick --skip
fi
CONFLICT_SKIPPED="${CONFLICT_SKIPPED}- \`${SHA}\` ${SUBJECT} ($REASON)"$'\n'
continue
fi
INCLUDED="${INCLUDED}- \`${SHA}\` ${SUBJECT}"$'\n'
else
POLICY_SKIPPED="${POLICY_SKIPPED}- \`${SHA}\` ${SUBJECT}"$'\n'
fi
done <<< "$ORDERED"
{
echo "## Cherry-pick summary"
echo ""
echo "Base: \`$BASE_TAG\` → Branch: \`$BRANCH\`$([ "$DRY_RUN" = "true" ] && echo " (DRY RUN — local only)")"
echo ""
if [ -n "$INCLUDED" ]; then
echo "### Included (fix/chore)"
echo ""
echo "$INCLUDED"
else
echo "_No fix/chore commits to include._"
fi
if [ -n "$NON_SHIPPED_SKIPPED" ]; then
echo "### Skipped — touches no shipped paths (informational)"
echo ""
echo "These fix/chore commits don't touch any path in the npm tarball's \`files\` whitelist (or \`package.json\`), so they cannot change the published package's behavior. CI / test / docs / planning-only changes belong on \`main\`, not in a hotfix. No action needed."
echo ""
echo "$NON_SHIPPED_SKIPPED"
fi
if [ -n "$CONFLICT_SKIPPED" ]; then
echo "### Skipped — cherry-pick conflict (manual review)"
echo ""
echo "$CONFLICT_SKIPPED"
fi
if [ -n "$POLICY_SKIPPED" ]; then
echo "### Not auto-included (feat/refactor/docs/etc)"
echo ""
echo "$POLICY_SKIPPED"
fi
} >> "$GITHUB_STEP_SUMMARY"
fi
fi
# Bump version on the branch (committed) so downstream install-smoke +
# release jobs build the correct version. The release job's own in-tree
# bump becomes a no-op when the file already has the right version.
CURRENT=$(node -p "require('./package.json').version")
if [ "$CURRENT" != "$VERSION" ]; then
npm version "$VERSION" --no-git-tag-version
git add package.json package-lock.json
if [ -f sdk/package.json ]; then
(cd sdk && npm version "$VERSION" --no-git-tag-version)
git add sdk/package.json
[ -f sdk/package-lock.json ] && git add sdk/package-lock.json
fi
git commit -m "chore: bump version to $VERSION for hotfix"
fi
if [ "$DRY_RUN" != "true" ]; then
git push origin "$BRANCH"
else
echo "DRY RUN — cherry-picks applied locally; branch not pushed. Downstream install-smoke will run against \`$BASE_TAG\` (the cherry-pick verification above is the dry-run signal)."
fi
- name: Determine effective ref
id: out
env:
ACTION: ${{ inputs.action }}
INPUT_REF: ${{ inputs.ref }}
DRY_RUN: ${{ inputs.dry_run }}
BASE_TAG: ${{ steps.hotfix.outputs.base_tag }}
BRANCH: ${{ steps.hotfix.outputs.branch }}
run: |
if [ "$ACTION" = "hotfix" ]; then
if [ "$DRY_RUN" = "true" ]; then
echo "ref=$BASE_TAG" >> "$GITHUB_OUTPUT"
else
echo "ref=$BRANCH" >> "$GITHUB_OUTPUT"
fi
else
echo "ref=$INPUT_REF" >> "$GITHUB_OUTPUT"
fi
# Cross-platform install validation gate (parity with release.yml).
# Publish job depends on this — won't proceed if the package fails to
# install cleanly across the supported matrix.
install-smoke:
needs: prepare
permissions:
contents: read
uses: ./.github/workflows/install-smoke.yml
with:
ref: ${{ inputs.ref }}
ref: ${{ needs.prepare.outputs.ref }}
release:
needs: install-smoke
needs: [prepare, install-smoke]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write # tag + push + GitHub Release
id-token: write # provenance
contents: write # tag + push + GitHub Release
id-token: write # provenance
# The merge-back PR step (and the pull-request scope it required)
# was removed in #2983 — auto-cherry-pick hotfix flow only picks
# commits already on main, so there's nothing to merge back.
environment: npm-publish
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
ref: ${{ inputs.ref }}
ref: ${{ needs.prepare.outputs.ref }}
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
@@ -90,10 +459,24 @@ jobs:
- name: Determine version
id: ver
env:
ACTION: ${{ inputs.action }}
INPUT_TAG: ${{ inputs.tag }}
INPUT_OVERRIDE: ${{ inputs.version }}
run: |
set -e
# Hotfix forces version=inputs.version and dist-tag=latest.
if [ "$ACTION" = "hotfix" ]; then
if [ -z "$INPUT_OVERRIDE" ]; then
echo "::error::action=hotfix requires the 'version' input"
exit 1
fi
VERSION="$INPUT_OVERRIDE"
EFFECTIVE_TAG="latest"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$EFFECTIVE_TAG" >> "$GITHUB_OUTPUT"
echo "→ Hotfix: will publish v${VERSION} to dist-tag '${EFFECTIVE_TAG}'"
exit 0
fi
RAW=$(node -p "require('./package.json').version")
BASE=$(echo "$RAW" | sed 's/-.*//')
if [ -n "$INPUT_OVERRIDE" ]; then
@@ -127,14 +510,21 @@ jobs:
echo "tag=$INPUT_TAG" >> "$GITHUB_OUTPUT"
echo "→ Will publish v${VERSION} to dist-tag '${INPUT_TAG}'"
- name: Refuse if version already exists on npm
# Reconciliation mode: if version is already on npm (a prior run
# published successfully but a downstream step failed), don't hard-fail.
# Set a flag and skip the publish step below; tag/release/PR/dist-tag
# steps still execute so the rerun can finish reconciling state.
- name: Detect prior publish (reconciliation mode)
id: prior_publish
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
EXISTING=$(npm view get-shit-done-cc@"$VERSION" version 2>/dev/null || true)
if [ -n "$EXISTING" ]; then
echo "::error::get-shit-done-cc@${VERSION} is already published. Bump version or pass an explicit override input."
exit 1
echo "::warning::get-shit-done-cc@${VERSION} is already on the registry — entering reconciliation mode (skip publish, continue with tag/release/PR/dist-tag)."
echo "skip_publish=true" >> "$GITHUB_OUTPUT"
else
echo "skip_publish=false" >> "$GITHUB_OUTPUT"
fi
# Tolerant tag-existence check (matches release.yml pattern). An
@@ -164,8 +554,11 @@ jobs:
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
npm version "$VERSION" --no-git-tag-version
cd sdk && npm version "$VERSION" --no-git-tag-version
# --allow-same-version: prepare may have already committed this bump
# on the hotfix branch (release checks out BRANCH in real runs,
# BASE_TAG in dry-runs — only the latter has the older version).
npm version "$VERSION" --no-git-tag-version --allow-same-version
cd sdk && npm version "$VERSION" --no-git-tag-version --allow-same-version
- name: Install dependencies
run: npm ci
@@ -234,6 +627,15 @@ jobs:
rm -f "$TARBALL"
- name: Dry-run publish validation
# Skip the rehearsal when the version is already on npm
# (reconciliation mode). `npm publish --dry-run` contacts the
# registry and fails with "You cannot publish over the
# previously published versions" if the version exists, even
# though no actual publish would be attempted. The real publish
# step (further down) is gated on the same condition; gate the
# rehearsal too so re-runs of an already-published hotfix don't
# fail here on a check that doesn't apply. Bug #2987.
if: ${{ steps.prior_publish.outputs.skip_publish != 'true' }}
env:
TAG: ${{ steps.ver.outputs.tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -252,7 +654,7 @@ jobs:
git push origin "v${VERSION}"
- name: Publish to npm (CC bundle, SDK included as both loose tree and .tgz)
if: ${{ !inputs.dry_run }}
if: ${{ !inputs.dry_run && steps.prior_publish.outputs.skip_publish != 'true' }}
env:
TAG: ${{ steps.ver.outputs.tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -271,7 +673,7 @@ jobs:
npm dist-tag add "get-shit-done-cc@${VERSION}" next
echo "✅ next dist-tag re-pointed to v${VERSION} (matches latest)"
- name: Create GitHub Release
- name: Create GitHub Release (idempotent)
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ github.token }}
@@ -281,7 +683,14 @@ jobs:
# Per-tag release flags:
# dev, next → --prerelease (won't be highlighted as the latest release on the repo page)
# latest → --latest (becomes the highlighted release)
if [ "$TAG" = "latest" ]; then
# Idempotent: if release already exists (rerun after a transient
# downstream failure), edit the latest flag instead of failing.
if gh release view "v${VERSION}" >/dev/null 2>&1; then
echo "GitHub Release v${VERSION} already exists; reconciling --latest flag"
if [ "$TAG" = "latest" ]; then
gh release edit "v${VERSION}" --latest || true
fi
elif [ "$TAG" = "latest" ]; then
gh release create "v${VERSION}" \
--title "v${VERSION}" \
--generate-notes \
@@ -292,7 +701,24 @@ jobs:
--generate-notes \
--prerelease
fi
echo "✅ GitHub Release v${VERSION} created"
echo "✅ GitHub Release v${VERSION} ready"
# Merge-back PR step removed — bug #2983.
#
# The auto-cherry-pick hotfix flow only picks commits already on
# main (`git cherry HEAD origin/main` outputs unmerged commits;
# we filter to fix:/chore: from main). By construction every code
# commit on the hotfix branch is already on main. The only
# hotfix-branch-only commit is `chore: bump version to X.Y.Z for
# hotfix`, which would either no-op against main (already past
# X.Y.Z) or rewind main's in-progress version — strictly
# counterproductive in either case.
#
# The original merge-back step also failed in production with
# `GitHub Actions is not permitted to create or approve pull
# requests (createPullRequest)` (org policy), but even if the
# policy were lifted the PR would have nothing useful to merge.
# Run 25232968975 was the trigger for removal.
- name: Verify publish landed on registry
if: ${{ !inputs.dry_run }}
@@ -322,23 +748,43 @@ jobs:
- name: Summary
env:
ACTION: ${{ inputs.action }}
VERSION: ${{ steps.ver.outputs.version }}
TAG: ${{ steps.ver.outputs.tag }}
BASE_TAG: ${{ needs.prepare.outputs.base_tag }}
BRANCH: ${{ needs.prepare.outputs.ref }}
DRY_RUN: ${{ inputs.dry_run }}
run: |
echo "## Release SDK Bundle: v${VERSION} → @${TAG}" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
if [ "$DRY_RUN" = "true" ]; then
echo "**DRY RUN** — npm publish, git tag, push, and GitHub Release were skipped." >> "$GITHUB_STEP_SUMMARY"
else
echo "- Published \`get-shit-done-cc@${VERSION}\` to dist-tag \`${TAG}\`" >> "$GITHUB_STEP_SUMMARY"
echo "- SDK bundled inside the CC tarball at:" >> "$GITHUB_STEP_SUMMARY"
echo " - \`sdk/dist/cli.js\` (loose tree, consumed by \`bin/gsd-sdk.js\` shim)" >> "$GITHUB_STEP_SUMMARY"
echo " - \`sdk-bundle/gsd-sdk.tgz\` (npm-installable artifact)" >> "$GITHUB_STEP_SUMMARY"
echo "- Git tag \`v${VERSION}\` pushed" >> "$GITHUB_STEP_SUMMARY"
echo "- GitHub Release \`v${VERSION}\` created" >> "$GITHUB_STEP_SUMMARY"
if [ "$TAG" = "latest" ]; then
echo "- \`next\` dist-tag re-pointed at \`v${VERSION}\` (kept current with \`latest\`)" >> "$GITHUB_STEP_SUMMARY"
{
if [ "$ACTION" = "hotfix" ]; then
echo "## Release SDK Bundle (hotfix): v${VERSION} → @${TAG}"
echo ""
echo "- Base (cumulative-fix anchor): \`${BASE_TAG}\`"
echo "- Branch: \`${BRANCH}\`"
else
echo "## Release SDK Bundle: v${VERSION} → @${TAG}"
fi
echo "- Install: \`npm install -g get-shit-done-cc@${TAG}\`" >> "$GITHUB_STEP_SUMMARY"
fi
echo ""
if [ "$DRY_RUN" = "true" ]; then
echo "**DRY RUN** — npm publish, git tag, push, and GitHub Release were skipped."
else
echo "- Published \`get-shit-done-cc@${VERSION}\` to dist-tag \`${TAG}\`"
echo "- SDK bundled inside the CC tarball at:"
echo " - \`sdk/dist/cli.js\` (loose tree, consumed by \`bin/gsd-sdk.js\` shim)"
echo " - \`sdk-bundle/gsd-sdk.tgz\` (npm-installable artifact)"
echo "- Git tag \`v${VERSION}\` pushed"
echo "- GitHub Release \`v${VERSION}\` created"
if [ "$TAG" = "latest" ]; then
echo "- \`next\` dist-tag re-pointed at \`v${VERSION}\` (kept current with \`latest\`)"
fi
if [ "$ACTION" = "hotfix" ]; then
# Auto-cherry-pick hotfixes only pick commits already on
# main, so there's nothing to merge back. The merge-back
# PR step was removed in #2983; this line surfaces the
# explicit non-action so operators don't expect a PR
# that was never opened.
echo "- No merge-back PR (auto-picked commits are already on main)"
fi
echo "- Install: \`npm install -g get-shit-done-cc@${TAG}\`"
fi
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -24,19 +24,20 @@ jobs:
echo "found=false" >> "$GITHUB_OUTPUT"
fi
- name: Comment and fail if no issue link
- name: Comment, close, and fail if no issue link
if: steps.check.outputs.found == 'false'
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
# Uses GitHub API SDK — no shell string interpolation of untrusted input
script: |
const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`;
const prNumber = context.payload.pull_request.number;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
issue_number: prNumber,
body: [
'## Missing issue link',
'## Missing issue link — PR auto-closed',
'',
'This PR does not reference an issue. **All PRs must link to an open issue** using a closing keyword in the PR body:',
'',
@@ -46,7 +47,13 @@ jobs:
'',
`If no issue exists for this change, [open one first](${repoUrl}/issues/new/choose), then update this PR body with the reference.`,
'',
'This PR will remain blocked until a valid `Closes #NNN`, `Fixes #NNN`, or `Resolves #NNN` line is present in the description.',
'To resume work after fixing the body: edit the PR description to add a valid `Closes #NNN`, `Fixes #NNN`, or `Resolves #NNN` line, then click **Reopen pull request**. The workflow will re-evaluate on reopen.',
].join('\n')
});
core.setFailed('PR body must contain a closing issue reference (e.g. "Closes #123")');
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed',
});
core.setFailed('PR body must contain a closing issue reference (e.g. "Closes #123") — PR closed.');

View File

@@ -30,6 +30,9 @@ jobs:
- name: Lint — no source-grep tests
shell: bash
run: node scripts/lint-no-source-grep.cjs
- name: Lint — command contract (ADR-0002)
shell: bash
run: node scripts/lint-command-contract.cjs
test:
runs-on: ${{ matrix.os }}
@@ -88,6 +91,18 @@ jobs:
- name: Build SDK dist (required by installer)
run: npm run build:sdk
# Seam contract gate: keep manifest -> generated aliases -> registry/CJS adapters aligned.
# Run once per workflow on the primary Linux node to avoid redundant matrix cost.
- name: SDK seam coverage tests
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24
shell: bash
run: cd sdk && npx vitest run src/query/command-seam-coverage.test.ts
- name: SDK generated alias artifact drift check
if: matrix.os == 'ubuntu-latest' && matrix.node-version == 24
shell: bash
run: node sdk/scripts/check-command-aliases-fresh.mjs
- name: Run tests with coverage
shell: bash
run: npm run test:coverage

1
.gitignore vendored
View File

@@ -66,3 +66,4 @@ vendor/
.cache/
tmp/
.worktrees
.envrc

View File

@@ -0,0 +1,104 @@
# Render agent definitions from templates at install/config-change time
**Source:** [#2758](https://github.com/gsd-build/get-shit-done/issues/2758)
**Decision:** wontfix — closed on the technical merits
**Date:** 2026-05-02
## Proposal summary
Move config-gated prose out of `agents/*.md` into `agents/templates/*.md.tmpl`,
rendered at install time and after `.planning/config.json` writes via a new
`gsd-sdk agents render` subcommand. Conditional branches resolve at render time
(deterministic code) instead of at inference time (LLM interpretation).
Three named benefits:
1. Token reduction proportional to disabled features.
2. Deterministic feature gating (impossible-by-construction vs. test-for).
3. Single source of truth for contributor-facing gating.
Cites PR #2279 (Codex/OpenCode model embedding at install time) as direct
precedent for compile-time embedding.
## Why GSD does not own this
### 1. The determinism claim is theoretical, not observed
The proposal's strongest argument is that config-gated branches in agent prose
are a determinism failure surface. The actual patterns in the codebase today are
already heavily mitigated:
- The `use_worktrees` branch in `gsd-executor` is resolved deterministically via
`gsd-sdk query config-get` in bash — it is not LLM-interpreted.
- "Skip if `workflow.X` is `false`" prose patterns are short, stable, and
follow a uniform "missing key = enabled" convention. There is no documented
history of LLMs running disabled checks or skipping enabled ones because of
this prose.
A theoretical failure surface should not be traded for a real, high-risk
patch-migration surface (`gsd-local-patches/` rebase logic, by the reporter's own
admission "the highest-risk piece of the change"). The reporter was asked for
documented evidence; none was provided.
### 2. Token waste is small and bounded
The codebase has roughly 5 `workflow.*` toggle references in agent files and
~20 "Skip if" conditional-prose patterns total — most 12 sentences. The
"real spend across multi-phase milestones" claim was not measured against
`gsd-context-monitor` output despite being asked. Without a measured baseline,
the token-savings argument is asserted rather than demonstrated, and the savings
ceiling on ~20 short conditionals is small enough that it does not justify a new
template-and-rendering subsystem with a CI-enforced template/generated split.
### 3. The deterministic-gating need is already served
PR #2279 established orchestrator-time config embedding for the cases that
genuinely need deterministic resolution (model selection, reasoning effort,
worktree mode). That mechanism is the right layer for orchestration-time
decisions and can be extended toggle-by-toggle along the existing path without
introducing a parallel templating subsystem. The proposal's own "Alternative #1"
(continue the orchestrator-embedding pattern) was rejected on the grounds that
agent-internal conditionals belong in the agent layer, but the asks behind the
proposal — determinism, lower token cost — are equally satisfied by extending
PR #2279 incrementally without a second mechanism.
Adding a templating layer alongside orchestrator-embedding means two mechanisms
own the same problem. The proposal does not specify a partition rule, and the
reporter did not respond when asked for one.
### 4. Patch-migration risk is disproportionate to benefit
The `/gsd-reapply-patches` three-way-merge migration for `gsd-local-patches/`
is, in the proposal's own words, the highest-risk piece of the change. It exists
solely to absorb a contributor-workflow shift — the user-facing surface is
unchanged. Risk that flows entirely from internal restructuring, where the
benefit is unmeasured token savings and a theoretical determinism gain, is the
wrong trade.
The reduced-scope variant (Alternative #5: fresh installs only, defer the
migration) avoids that specific risk but still ships a parallel mechanism for
benefits that remain unmeasured and that PR #2279's path can absorb.
## Re-open criteria
This may be revisited if a contributor:
- Provides measured token deltas via `gsd-context-monitor` against a
representative all-toggles-off config, and the delta is materially larger
than what extending PR #2279's orchestrator-embedding path one toggle at a
time would produce.
- Documents a real LLM misinterpretation of an existing toggle conditional
(executor ignored `workflow.use_worktrees: false`, verifier ran when
`workflow.verifier: false`, etc.) — not a projected failure mode.
- Proposes a clear partition rule between orchestrator-time embedding (PR #2279)
and any new install-time templating layer, so the two mechanisms do not
overlap.
## Related
- PR #2279 — Codex/OpenCode model embedding at install time (the established
precedent for deterministic compile-time embedding into agent files)
- v1.37.0 release notes — shared-boilerplate extraction (reference files for
mandatory-initial-read, project-skills-discovery)
- `get-shit-done/workflows/` — workflow-level config embedding before subagent
spawn (the path of least friction for incremental deterministic gating)

View File

@@ -0,0 +1,56 @@
# Temporal context as a first-class GSD signal
**Source:** [#2756](https://github.com/gsd-build/get-shit-done/issues/2756)
**Decision:** wontfix — closed without further engagement
**Date:** 2026-05-02
## Proposal summary
Reporter proposed treating idle-time-between-turns as a first-class context signal in
GSD. Three flavors floated across the issue:
1. **Passive** — block at session resume injecting "you've been idle Nh, here's what was
open" into the orchestrator prompt.
2. **Active**`/resume-context` slash command.
3. **Retrospective**`HANDOFF.json` written at session end, read at next start.
Framed initially as a `claude-inject-idle-time` plugin, with a request that GSD treat
the pattern as core.
## Why GSD does not own this
- **Subagent gap unsolved.** Passive injection lands in the orchestrator's context
only. Subagents (the workers that actually do GSD's planning, execution, verification)
spawn fresh and never see the temporal signal. The proposal does not solve this, and
any GSD-core integration would inherit the gap. Until the subagent boundary is
addressed, "first-class temporal context" is at best a partial feature.
- **`HANDOFF.json` duplicates existing artifacts.** GSD already persists session
continuity through `.planning/state/*` and per-phase artifacts (PLAN.md, RESEARCH.md,
REVIEW.md, VERIFICATION.md). A separate handoff file would either drift from those or
redundantly mirror them. The right primitive for "what was I doing" already exists.
- **Statusline / TUI re-entry is platform-level, not GSD-level.** A statusline showing
idle time belongs in Claude Code itself or in a thin user plugin, not in GSD's phase
machinery.
- **Scope is unstable.** Reporter agreed with the narrowed minimum ask ("doc mention
only, rest opt-in"), then partially retracted it in a follow-up comment ("very
integral to myself"). The maintainer asked which version of the ask should move
forward; reporter did not respond.
## Re-open criteria
This may be revisited if a reporter:
- Engages with the subagent-gap problem and proposes a concrete mechanism for
temporal context to reach subagents (not just the orchestrator).
- Demonstrates a use case `.planning/state/*` provably cannot serve.
- Commits to a single stable scope (doc mention OR core integration OR plugin
reference) rather than oscillating between them mid-thread.
A drive-by enhancement request that the author does not return to engage with after
maintainer questions is not actionable. Future proposers: please plan to participate
through to a triage decision rather than dropping an issue and moving on.
## Related
- `.planning/state/` — existing session-continuity artifacts
- `get-shit-done/references/` — where any future plugin-interface doc would live

Some files were not shown because too many files have changed in this diff Show More