mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-05-08 16:22:14 +02:00
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:
4
.changeset/3166-graphify-inline-build.md
Normal file
4
.changeset/3166-graphify-inline-build.md
Normal 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`.
|
||||
4
.changeset/3170-graphify-commit-staleness.md
Normal file
4
.changeset/3170-graphify-commit-staleness.md
Normal 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 4–40 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
44
.changeset/README.md
Normal 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.
|
||||
11
.changeset/adr-0002-command-contract-validation.md
Normal file
11
.changeset/adr-0002-command-contract-validation.md
Normal 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.
|
||||
5
.changeset/blue-stones-topology.md
Normal file
5
.changeset/blue-stones-topology.md
Normal 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.
|
||||
5
.changeset/bold-finches-rally.md
Normal file
5
.changeset/bold-finches-rally.md
Normal 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.
|
||||
8
.changeset/brave-mice-build.md
Normal file
8
.changeset/brave-mice-build.md
Normal 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).
|
||||
6
.changeset/bright-pumas-fold.md
Normal file
6
.changeset/bright-pumas-fold.md
Normal 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.
|
||||
5
.changeset/calm-birds-greet.md
Normal file
5
.changeset/calm-birds-greet.md
Normal 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.
|
||||
5
.changeset/calm-ibex-jump.md
Normal file
5
.changeset/calm-ibex-jump.md
Normal 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.
|
||||
5
.changeset/calm-tigers-frolic.md
Normal file
5
.changeset/calm-tigers-frolic.md
Normal 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)
|
||||
5
.changeset/codex-bare-node-fix.md
Normal file
5
.changeset/codex-bare-node-fix.md
Normal 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.
|
||||
5
.changeset/codex-discuss-fallback.md
Normal file
5
.changeset/codex-discuss-fallback.md
Normal 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.
|
||||
6
.changeset/cool-monkeys-smell.md
Normal file
6
.changeset/cool-monkeys-smell.md
Normal 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.
|
||||
5
.changeset/curious-bears-march.md
Normal file
5
.changeset/curious-bears-march.md
Normal 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.
|
||||
5
.changeset/docs-1-40-0-audit.md
Normal file
5
.changeset/docs-1-40-0-audit.md
Normal 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.
|
||||
5
.changeset/dynamic-routing.md
Normal file
5
.changeset/dynamic-routing.md
Normal 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.
|
||||
5
.changeset/eager-badgers-purr.md
Normal file
5
.changeset/eager-badgers-purr.md
Normal 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.
|
||||
5
.changeset/eager-hawks-rally.md
Normal file
5
.changeset/eager-hawks-rally.md
Normal 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.
|
||||
5
.changeset/fix-3054-doc-anchor-and-token-check.md
Normal file
5
.changeset/fix-3054-doc-anchor-and-token-check.md
Normal 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.
|
||||
5
.changeset/fix-3056-worktree-path-assertion.md
Normal file
5
.changeset/fix-3056-worktree-path-assertion.md
Normal 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.
|
||||
5
.changeset/fix-3072-findings-probe-assertions.md
Normal file
5
.changeset/fix-3072-findings-probe-assertions.md
Normal 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.
|
||||
5
.changeset/fix-3087-planner-directive-language.md
Normal file
5
.changeset/fix-3087-planner-directive-language.md
Normal 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.4–v1.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.
|
||||
5
.changeset/fix-3088-milestone-state-fallback-sections.md
Normal file
5
.changeset/fix-3088-milestone-state-fallback-sections.md
Normal 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`.
|
||||
5
.changeset/fix-3094-progress-stale-assumptions.md
Normal file
5
.changeset/fix-3094-progress-stale-assumptions.md
Normal 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.
|
||||
5
.changeset/fix-3096-ai-integration-parallel-race.md
Normal file
5
.changeset/fix-3096-ai-integration-parallel-race.md
Normal 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.
|
||||
11
.changeset/fix-3097-3099-executor-worktree-path.md
Normal file
11
.changeset/fix-3097-3099-executor-worktree-path.md
Normal 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.
|
||||
5
.changeset/fix-3120-secure-phase-empty-register.md
Normal file
5
.changeset/fix-3120-secure-phase-empty-register.md
Normal 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.
|
||||
5
.changeset/fix-3121-gsd-tools-commands-verb.md
Normal file
5
.changeset/fix-3121-gsd-tools-commands-verb.md
Normal 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.
|
||||
5
.changeset/fix-3126-global-skills-base-runtime.md
Normal file
5
.changeset/fix-3126-global-skills-base-runtime.md
Normal 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.
|
||||
5
.changeset/fix-3127-state-begin-phase-idempotent.md
Normal file
5
.changeset/fix-3127-state-begin-phase-idempotent.md
Normal 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.
|
||||
5
.changeset/fix-3128-roadmap-plan-count-slug.md
Normal file
5
.changeset/fix-3128-roadmap-plan-count-slug.md
Normal 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.
|
||||
5
.changeset/fix-3129-validate-commit-bypass.md
Normal file
5
.changeset/fix-3129-validate-commit-bypass.md
Normal 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.
|
||||
5
.changeset/fix-3130-update-npx-robust.md
Normal file
5
.changeset/fix-3130-update-npx-robust.md
Normal 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.
|
||||
5
.changeset/fix-3135-capture-backlog-workflow.md
Normal file
5
.changeset/fix-3135-capture-backlog-workflow.md
Normal 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.
|
||||
5
.changeset/fix-3150-stats-json-decimal-gap-regression.md
Normal file
5
.changeset/fix-3150-stats-json-decimal-gap-regression.md
Normal 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.
|
||||
5
.changeset/fix-3153-statusline-percent-next-phases.md
Normal file
5
.changeset/fix-3153-statusline-percent-next-phases.md
Normal 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.
|
||||
5
.changeset/gemini-skip-local-when-global.md
Normal file
5
.changeset/gemini-skip-local-when-global.md
Normal 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.
|
||||
5
.changeset/happy-jays-greet.md
Normal file
5
.changeset/happy-jays-greet.md
Normal 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.
|
||||
5
.changeset/happy-tigers-travel.md
Normal file
5
.changeset/happy-tigers-travel.md
Normal 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.
|
||||
5
.changeset/help-passthrough.md
Normal file
5
.changeset/help-passthrough.md
Normal 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.
|
||||
5
.changeset/humble-goats-swim.md
Normal file
5
.changeset/humble-goats-swim.md
Normal 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.
|
||||
5
.changeset/install-shell-path-probe.md
Normal file
5
.changeset/install-shell-path-probe.md
Normal 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.
|
||||
5
.changeset/issue-driven-orchestration.md
Normal file
5
.changeset/issue-driven-orchestration.md
Normal 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.
|
||||
5
.changeset/jolly-newts-roam.md
Normal file
5
.changeset/jolly-newts-roam.md
Normal 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.
|
||||
5
.changeset/jolly-pumas-dance.md
Normal file
5
.changeset/jolly-pumas-dance.md
Normal 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.
|
||||
5
.changeset/lively-goats-run.md
Normal file
5
.changeset/lively-goats-run.md
Normal 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.
|
||||
5
.changeset/lively-moles-caper.md
Normal file
5
.changeset/lively-moles-caper.md
Normal 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.
|
||||
5
.changeset/lively-otters-gather.md
Normal file
5
.changeset/lively-otters-gather.md
Normal 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)
|
||||
5
.changeset/mcp-token-budget-docs.md
Normal file
5
.changeset/mcp-token-budget-docs.md
Normal 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.
|
||||
5
.changeset/merry-foxes-climb.md
Normal file
5
.changeset/merry-foxes-climb.md
Normal 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.
|
||||
5
.changeset/merry-lynx-sing.md
Normal file
5
.changeset/merry-lynx-sing.md
Normal 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.
|
||||
5
.changeset/merry-lynx-wander.md
Normal file
5
.changeset/merry-lynx-wander.md
Normal 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).
|
||||
5
.changeset/merry-moles-chatter.md
Normal file
5
.changeset/merry-moles-chatter.md
Normal 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.
|
||||
5
.changeset/noble-badgers-roar.md
Normal file
5
.changeset/noble-badgers-roar.md
Normal 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.
|
||||
5
.changeset/per-phase-type-models.md
Normal file
5
.changeset/per-phase-type-models.md
Normal 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.
|
||||
5
.changeset/plucky-ibex-gather.md
Normal file
5
.changeset/plucky-ibex-gather.md
Normal 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.
|
||||
5
.changeset/plucky-moles-roam.md
Normal file
5
.changeset/plucky-moles-roam.md
Normal 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.
|
||||
5
.changeset/plucky-otters-roam.md
Normal file
5
.changeset/plucky-otters-roam.md
Normal 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.
|
||||
5
.changeset/plucky-pandas-sprint.md
Normal file
5
.changeset/plucky-pandas-sprint.md
Normal 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.
|
||||
5
.changeset/pr-3112-release-note.md
Normal file
5
.changeset/pr-3112-release-note.md
Normal 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.
|
||||
5
.changeset/pr-3113-release-note.md
Normal file
5
.changeset/pr-3113-release-note.md
Normal 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.
|
||||
5
.changeset/pr-3115-release-note.md
Normal file
5
.changeset/pr-3115-release-note.md
Normal 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.
|
||||
5
.changeset/pr-3116-release-note.md
Normal file
5
.changeset/pr-3116-release-note.md
Normal 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.
|
||||
5
.changeset/pr-3118-release-note.md
Normal file
5
.changeset/pr-3118-release-note.md
Normal 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.
|
||||
5
.changeset/pr-3123-release-note.md
Normal file
5
.changeset/pr-3123-release-note.md
Normal 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.
|
||||
5
.changeset/pr-3124-release-note.md
Normal file
5
.changeset/pr-3124-release-note.md
Normal 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.
|
||||
5
.changeset/pr-3125-release-note.md
Normal file
5
.changeset/pr-3125-release-note.md
Normal 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.
|
||||
5
.changeset/quick-geese-hum.md
Normal file
5
.changeset/quick-geese-hum.md
Normal 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.
|
||||
5
.changeset/rapid-goats-munch.md
Normal file
5
.changeset/rapid-goats-munch.md
Normal 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.
|
||||
5
.changeset/research-flag-and-stale-refs.md
Normal file
5
.changeset/research-flag-and-stale-refs.md
Normal 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.
|
||||
6
.changeset/rewire-orphaned-workflows-3131.md
Normal file
6
.changeset/rewire-orphaned-workflows-3131.md
Normal 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.
|
||||
5
.changeset/scrub-stale-command-routes.md
Normal file
5
.changeset/scrub-stale-command-routes.md
Normal 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.
|
||||
5
.changeset/silly-foxes-wander.md
Normal file
5
.changeset/silly-foxes-wander.md
Normal 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.
|
||||
5
.changeset/silly-newts-swim.md
Normal file
5
.changeset/silly-newts-swim.md
Normal 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.
|
||||
6
.changeset/steady-ravens-shape.md
Normal file
6
.changeset/steady-ravens-shape.md
Normal 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.
|
||||
5
.changeset/sturdy-jays-glide.md
Normal file
5
.changeset/sturdy-jays-glide.md
Normal 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.
|
||||
5
.changeset/swift-coyotes-document.md
Normal file
5
.changeset/swift-coyotes-document.md
Normal 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.
|
||||
5
.changeset/tidy-tunas-zip.md
Normal file
5
.changeset/tidy-tunas-zip.md
Normal 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.
|
||||
5
.changeset/typed-rivers-flow.md
Normal file
5
.changeset/typed-rivers-flow.md
Normal 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.
|
||||
5
.changeset/update-banner-opt-in.md
Normal file
5
.changeset/update-banner-opt-in.md
Normal 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`.
|
||||
5
.changeset/witty-hawks-jump.md
Normal file
5
.changeset/witty-hawks-jump.md
Normal 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.
|
||||
5
.changeset/witty-newts-greet.md
Normal file
5
.changeset/witty-newts-greet.md
Normal 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.
|
||||
5
.changeset/zesty-jays-wake.md
Normal file
5
.changeset/zesty-jays-wake.md
Normal 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.
|
||||
5
.changeset/zesty-moles-forage.md
Normal file
5
.changeset/zesty-moles-forage.md
Normal 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
26
.coderabbit.yaml
Normal 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
6
.githooks/pre-commit
Executable 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
48
.githooks/pre-push
Executable 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
|
||||
2
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
2
.github/PULL_REQUEST_TEMPLATE/enhancement.md
vendored
@@ -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
|
||||
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE/feature.md
vendored
2
.github/PULL_REQUEST_TEMPLATE/feature.md
vendored
@@ -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)
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE/fix.md
vendored
2
.github/PULL_REQUEST_TEMPLATE/fix.md
vendored
@@ -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
|
||||
|
||||
13
.github/workflows/canary.yml
vendored
13
.github/workflows/canary.yml
vendored
@@ -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"
|
||||
|
||||
24
.github/workflows/changeset-required.yml
vendored
Normal file
24
.github/workflows/changeset-required.yml
vendored
Normal 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
|
||||
366
.github/workflows/hotfix.yml
vendored
366
.github/workflows/hotfix.yml
vendored
@@ -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"
|
||||
|
||||
524
.github/workflows/release-sdk.yml
vendored
524
.github/workflows/release-sdk.yml
vendored
@@ -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"
|
||||
|
||||
17
.github/workflows/require-issue-link.yml
vendored
17
.github/workflows/require-issue-link.yml
vendored
@@ -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.');
|
||||
|
||||
15
.github/workflows/test.yml
vendored
15
.github/workflows/test.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -66,3 +66,4 @@ vendor/
|
||||
.cache/
|
||||
tmp/
|
||||
.worktrees
|
||||
.envrc
|
||||
|
||||
104
.out-of-scope/agent-template-rendering.md
Normal file
104
.out-of-scope/agent-template-rendering.md
Normal 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 1–2 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)
|
||||
56
.out-of-scope/temporal-context.md
Normal file
56
.out-of-scope/temporal-context.md
Normal 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
Reference in New Issue
Block a user