mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-05-13 18:46:38 +02:00
a33cbe72f569e75e72d94de85d0930296d1ca1df
53 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
397c34142a |
Deepen SDK package seam and converge runtime skills policy (#3238)
* Deepen SDK package seam and converge runtime skills policy * fix(sdk): unified install-root resolution for workflows and agents (CR finding 1) Use the already-resolved gsdInstallDir constant instead of calling resolveLegacyInstallDir() again when computing agentsDir, ensuring workflowsDir and agentsDir share the same install root. * fix(sdk): tilde shortening requires path-boundary match (CR finding 2) Both renderGlobalSkillsBaseDisplayPath and renderGlobalSkillDisplayPath used startsWith(home) which could incorrectly shorten unrelated paths sharing the same prefix. Now checks for home === base or base.startsWith(home + sep) to ensure a real directory boundary. * fix(sdk): validate loadConfig export before invocation (CR finding 3) After requiring core.cjs, check typeof mod.loadConfig === 'function' before calling it. Throws a classified GSDError with the module path if the export is missing, rather than a generic TypeError. * fix(test): guard root lookup before .path dereference (CR finding 4) Added assert.ok() guards for claudeRoot and codexRoot after the .find() calls so that a missing root produces an explicit assertion failure rather than a TypeError on .path dereference. * fix(ci): fail-safe on transient API errors in approval dismissal (CR finding 6) resolveRole() returns 'unknown' for non-404 errors (rate limits, 5xx, network blips). shouldDismissReviewer() now treats 'unknown' as unresolvable and skips dismissal, preventing legitimate approvals from being dismissed due to a transient API failure. Only 'none' (true 404) is treated as a confirmed non-collaborator. * changeset: pr=3238 SDK package seam and runtime skills convergence * fix(sdk): harden resolveGlobalSkillDir against path traversal (CR finding 1) Use resolve+relative to validate that skillName cannot escape the global skills base directory. Values like "../../foo" or absolute paths now return null instead of joining directly. All imports (resolve, relative, isAbsolute) were already present in helpers.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): split skill-dir-resolution and skill-not-found warnings (CR finding 2) After resolveGlobalSkillDir's hardening can return null for traversal attempts, the old single-branch warning "Global skill not found at ..." was misleading. Split into two distinct cases: - skillDir === null → "Could not resolve global skill directory for ..." - skillMd missing → "Global skill not found at ..." Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: lock skill path-traversal rejection in resolveGlobalSkillDir Regression test verifying that traversal segments (../../foo, ../escape), empty string, and absolute paths are all rejected (return null), while a legitimate skill name resolves correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(sdk): align display-path contract + traversal coverage for resolveGlobalSkillMarkdownPath (CR nitpicks) - renderGlobalSkillsBaseDisplayPath now returns a non-null string for unsupported runtimes (e.g. cline → "(cline does not use a skills directory)") matching the existing renderGlobalSkillDisplayPath contract; callers of both helpers no longer need null-checks for unsupported runtimes. - Remove now-redundant ! non-null assertion on renderGlobalSkillsBaseDisplayPath calls in skill-manifest.ts (return type is string, not string | null). - Extend the path-traversal test block to assert resolveGlobalSkillMarkdownPath also propagates null for ../../foo, ../escape, empty, and /abs/path inputs, locking the null-propagation contract against future refactors. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
73f7ad33e8 | ci: limit unauthorized approval dismissal to open PRs | ||
|
|
9ae2b2abae | ci: batch unauthorized approval sweep | ||
|
|
66e686d1fd | ci: add workflow to dismiss unauthorized PR approvals | ||
|
|
81f9534b5a |
feat(adr-0002): command contract validation module + prose @-ref cleanup + workflow extraction
ADR-0002: commands/gsd/*.md contract now enforced at two layers: LINT (scripts/lint-command-contract.cjs — new CI step): - name: present, starts with gsd: or gsd- - description: non-empty - allowed-tools: non-empty, all entries canonical - execution_context @-refs: resolve on disk, no trailing prose on same line - handles both @~/ and $HOME/ path prefixes TEST (tests/command-contract.test.cjs — 361 assertions): - Behavioral contract for all 65 command files - Replaces scattered coverage in enh-2790 + bug-3135 - Per-command per-rule test — one failure names the exact file + rule CI (.github/workflows/test.yml): - 'Lint — command contract (ADR-0002)' step added to lint-tests job PROSE @-REF CLEANUP (39 command files, ~900 tokens/invocation recovered): - Removed redundant @~/.claude/get-shit-done/... paths from <process> prose - execution_context block is now the single authoritative load declaration - Routing commands (sketch, spike, update, pause-work, etc.) keep routing instructions; only the inert path token is stripped WORKFLOW EXTRACTION (debug.md + thread.md, ~15,000 chars / ~3,750 tokens): - get-shit-done/workflows/debug.md: full process extracted from commands/gsd/debug.md - get-shit-done/workflows/thread.md: full process extracted from commands/gsd/thread.md - Command files reduced to frontmatter + objective + execution_context + context - debug.md: 9,603 → 1,703 chars; thread.md: 7,868 → 585 chars RENAME: - get-shit-done/workflows/extract_learnings.md → extract-learnings.md (aligns with hyphen convention of all other workflow files) DOCS: - docs/INVENTORY.md: count 85→87, new rows, rename row, fix add-todo --backlog attribution - docs/INVENTORY-MANIFEST.json: +debug.md +thread.md +extract-learnings.md -extract_learnings.md Closes ADR-0002 implementation. |
||
|
|
9d5db87249 |
feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts (#2978)
* feat(#2975): adopt changeset-fragment workflow to eliminate CHANGELOG conflicts Two PRs that both edit `### Fixed` in CHANGELOG.md always conflict on merge. Recently bit on #2960/#2972 in the same session — fix-the-conflict-and-rebase tax. Replace the shared-file model with per-PR fragment files that never share lines. Implementation built TDD per #2975, vertical slices with structured-IR assertions throughout: scripts/changeset/parse.cjs - fragment text → typed record + frozen FRAGMENT_ERROR enum (8 tests) scripts/changeset/render.cjs - fragments → structured IR with Keep-a-Changelog section ordering (2 tests) scripts/changeset/serialize.cjs - IR ↔ markdown round-trip pair (parse(serialize(ir)) === ir, 3 tests) scripts/changeset/cli.cjs - file-I/O wrapper with --json mode; reads .changeset/, folds into CHANGELOG.md, deletes consumed fragments. Idempotent. (1 test) scripts/changeset/lint.cjs - pure verdict (changedFiles, labels) → { ok, reason } via LINT_REASON enum. Honors `no-changelog` label. (5 tests) scripts/changeset/new.cjs - fragment scaffolder with random adjective-noun-noun filename. Tests assert via parseFragment round-trip. (3 tests) Total: 22 tests, all assertions on typed structured fields. No regex on text, no String#includes on file content. Lint clean across 356 test files. Supporting: .changeset/README.md - format spec + workflow docs .changeset/eager-hawks-rally.md - dogfood fragment for THIS PR (will be the first thing the new release tool consumes) .github/workflows/changeset-required.yml - CI: every PR runs lint.cjs package.json - npm run changeset, changelog:render, lint:changeset CONTRIBUTING.md - new "CHANGELOG Entries — Drop a Fragment" section between PR Guidelines and Testing Standards Closes #2975 * fix(#2975): address CodeRabbit findings on changeset workflow 7 valid findings (4 Major, 3 Minor); all addressed: scripts/changeset/parse.cjs - Preserve fragment body verbatim. Previously body.trim() ate intentional leading whitespace (code blocks, etc.); now trim() is used only for the emptiness check, and a single trailing newline is stripped (the editor-added one) so well-formed fragments round-trip byte-for-byte. Added a regression test asserting a code-block-leading body is preserved. scripts/changeset/cli.cjs - Validate flag values during argument parsing. parseArgs now returns { ok, opts | error }; rejects `--repo` etc. with no following value or with another flag as the value. main() surfaces the error message before exiting 2. - Handle post-write fragment-deletion failures. After CHANGELOG.md is written, any unlink failure is captured into a structured deleteFailures list with reason 'fail_fragment_delete'; cmdRender returns exitCode=1 with the partial-failure detail instead of leaving the changelog updated and fragments behind (which would cause double-consumption on rerun). scripts/changeset/lint.cjs - Treat CHANGELOG.md as a linted user-facing path. Direct edits to CHANGELOG.md (the bypass route around the new workflow) now fail the lint with FAIL_MISSING_FRAGMENT. Added a regression test for that case. - Use cp.execFileSync instead of cp.execSync for the git diff call. Eliminates the shell-interpolation surface on GITHUB_BASE_REF; git's own arg parser remains the validator. scripts/changeset/new.cjs - Atomic fragment creation. existsSync() + writeFileSync was racy under concurrent invocations. Now writeFileSync uses { flag: 'wx' } which fails EEXIST on collision; the random-name retry loop catches EEXIST and re-rolls. Throws explicitly after 16 attempts rather than silently overwriting. .changeset/README.md - Add language tag `md` to the format example fence (markdownlint MD040). All 25 changeset tests pass; lint clean (356 test files, 0 violations). * fix(#2975): sanitize --type and validate flag values in new.cjs (CR fixes) Two CR findings on scripts/changeset/new.cjs: 1. (Minor) `type` was embedded in frontmatter without sanitization. A newline in the value (e.g. `--type 'Fixed\ntype: Added'`) would corrupt the fragment. scaffoldFragment now validates `type` against the Keep-a-Changelog ALLOWED_TYPES set BEFORE writing — same set parse.cjs uses on consume. Throws with a typed error referencing the allowed values; tests cover the newline case + 4 other non-allowed values. 2. (Minor) `--repo` (and other value-taking flags) without a value silently set opts.repo to undefined, which produced a cryptic ERR_INVALID_ARG_TYPE deep inside path.join. parseArgs now mirrors the cli.cjs convention: returns { ok, opts | error }, validates that the next token exists and is not itself another flag, and surfaces a precise "missing value for --repo" message before exit. Added 3 tests: missing-trailing-value, flag-as-value, well-formed. 29 tests pass across the changeset suite (4 new regression tests). |
||
|
|
cb98a88139 |
fix(#2987): skip dry-run publish validation when version is already on npm (#2988)
The `Dry-run publish validation` step ran `npm publish --dry-run` with
no `if:` guard. `npm publish --dry-run` contacts the registry and
exits 1 with "You cannot publish over the previously published
versions" when the target version exists.
The earlier `Detect prior publish (reconciliation mode)` step already
discovers this case and sets steps.prior_publish.outputs.skip_publish=true.
The actual publish step (further down) is gated on that. The
rehearsal step was missing the gate, so any re-run of an
already-published hotfix blew up at the rehearsal before reaching
the reconciliation logic — exactly when an operator is trying to
recover from a later-step failure (merge-back, summary, etc.).
Add `if: ${{ steps.prior_publish.outputs.skip_publish != 'true' }}`
matching the publish step's gate. The rehearsal still runs on first
publishes where it has value.
Trigger: run 25233855236.
Closes #2987
|
||
|
|
fb92d1e596 |
fix(#2983): classifier exit-code discipline, base-tag staging, drop vestigial merge-back (#2984)
* fix(#2983): classifier exit-code discipline, base-tag staging, drop vestigial merge-back Three issues surfaced by CodeRabbit's post-merge review of #2981 plus a production failure on the v1.39.1 release run. (1) Overloaded classifier exit code scripts/diff-touches-shipped-paths.cjs reused exit 1 for both the legitimate "no shipped paths" result and Node's default exit on uncaught throw, so any classifier failure (corrupt package.json, EPERM, etc.) was indistinguishable from a normal skip — the workflow's `if ! ... ; then skip` idiom would silently drop the commit. Distinct exit codes now: 0 shipped — at least one path is in the npm `files` whitelist 1 not shipped — CI / test / docs / planning only 2 classifier error — workflow MUST fail-fast uncaughtException + unhandledRejection + try/catch around fs/JSON parsing all route to exit 2 with stderr context. (2) Classifier missing at the base tag (CRITICAL) `Prepare hotfix branch` runs `git checkout -b "$BRANCH" "$BASE_TAG"` BEFORE the cherry-pick loop, replacing the working tree with the base tag's contents. Base tags predating #2980 (notably v1.39.0, the most likely next hotfix base) don't have scripts/diff-touches-shipped-paths.cjs at all — `node <missing>` exits non-zero — `if !` skips every commit — empty hotfix branch published. Strictly worse than the original #2980 push-rejection, which at least failed loudly. Stage the classifier from the dispatched ref's working tree into $RUNNER_TEMP at the top of the run script (before any working-tree- mutating git command). The cherry-pick loop now references $CLASSIFIER (staged) instead of the in-tree path. Sanity guards: refuse to start if scripts/diff-touches-shipped-paths.cjs is missing in the dispatched ref, refuse to proceed if cp didn't materialize $CLASSIFIER. The cherry-pick loop captures node's exit via ${PIPESTATUS[1]} and dispatches via explicit case: 0 proceed with cherry-pick 1 skip into NON_SHIPPED_SKIPPED * emit ::error:: + exit "$CLASSIFIER_RC" (3) Drop the merge-back PR step Auto-cherry-pick only picks commits already on main (`git cherry HEAD origin/main` outputs the unmerged ones; we filter 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 either no-ops against main or rewinds main's in-progress version. The merge-back PR was vestigial. It also failed in production on run 25232968975 with `GitHub Actions is not permitted to create or approve pull requests (createPullRequest)` — org policy blocks PR creation from the workflow's GH_TOKEN. Even without that block, the PR would have nothing useful to merge. Step removed. The `pull-requests: write` permission granted solely for the merge-back step has been dropped from the release job (least-privilege). Regression coverage tests/bug-2983-classifier-exit-codes-and-base-tag-staging.test.cjs adds 12 assertions across two describe blocks: - 5 classifier behavioral: exit 0/1 preserved, exit 2 on missing package.json, exit 2 on malformed JSON, exit-code constants exported. - 7 workflow contract: classifier staged before checkout, target is $RUNNER_TEMP, missing-source guard, missing-staged guard, PIPESTATUS-based dispatch, error branch fails workflow, loop uses staged path (not in-tree). tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs updated where it asserted the pre-#2983 `if ! ... ; then` shape: now accepts the post-#2983 case-dispatch form. The test still proves the classifier participates; bug-2983 enforces the specific shape. Run summary references for the curious reviewer: - Run 25232010071 — original #2980 trigger (workflow-file push rejection) - Run 25232968975 — failed merge-back step that prompted the "is this even useful?" question that drove the removal Closes #2983 * fix(#2983): address CodeRabbit findings on PR #2984 Two findings, both real, both fixed. (1) [Critical] PIPESTATUS capture clobbered by `|| true` Pre-fix shape: git diff-tree ... | node "$CLASSIFIER" || true CLASSIFIER_RC="${PIPESTATUS[1]}" When the classifier exits 1 ("not shipped" — common case) or 2 (error), `|| true` triggers the right-hand side. `true` is a one-command "pipeline" that overwrites PIPESTATUS to (0). ${PIPESTATUS[1]} on the next line is therefore unset (or stale under set -u). The case dispatch then matched the empty string — falling into `*)` and failing the workflow on every non-shipped commit, OR matching `0)` after some shells default-init unset to 0 and silently picking commits that don't ship. Local repro confirms the issue: $ bash -c 'set -euo pipefail; false | sh -c "exit 7" || true; \ echo "PIPESTATUS: ${PIPESTATUS[*]}"; \ echo "[1]: ${PIPESTATUS[1]:-<unset>}"' PIPESTATUS: 0 [1]: <unset> Fix: bracket the pipeline in `set +e`/`set -e`, snapshot PIPESTATUS into a local array on the very next line, then dispatch on the snapshot: set +e git diff-tree ... | node "$CLASSIFIER" PIPE_RC=("${PIPESTATUS[@]}") set -e DIFFTREE_RC="${PIPE_RC[0]}" CLASSIFIER_RC="${PIPE_RC[1]}" The snapshot must happen on the first line after the pipeline; any intervening simple command resets PIPESTATUS. The array form is invariant against that. Bonus from the new shape: $DIFFTREE_RC is now also captured. git diff-tree is unlikely to fail on a known-good $SHA, but if it does, we no longer feed partial/empty input to the classifier and call it "not shipped." A non-zero DIFFTREE_RC emits ::error::git diff-tree failed and exits. (2) [Minor] Stale "Merge-back PR opened against main" summary line The hotfix run summary still printed: echo "- Merge-back PR opened against main" But the merge-back step itself was removed in the previous commit on this branch. Operators reading the summary would expect a PR that doesn't exist. Replaced with explicit non-action text: echo "- No merge-back PR (auto-picked commits are already on main)" Test coverage bug-2983 test file gains 3 assertions: - PIPE_RC array-snapshot pattern is required (regex matches the exact `PIPE_RC=("${PIPESTATUS[@]}")` form). - The `pipeline || true; ${PIPESTATUS[1]}` antipattern is explicitly forbidden via assert.doesNotMatch. - DIFFTREE_RC is captured from PIPE_RC[0] and a non-zero value triggers ::error::git diff-tree failed. - Run summary forbids `Merge-back PR opened against main` and requires the new non-action sentence. bug-2964 test's loop-anchor window bumped 6 KB → 8 KB to accommodate the additional pre-pick scaffolding (the test's own comment had already anticipated this kind of growth, citing prior precedents from #2970 and #2980). Mark CodeRabbit comments resolved post-commit. Refs CR finding ids 3175253571, 3175253578 on PR #2984. |
||
|
|
7424271aa0 |
fix(#2980): hotfix cherry-pick only picks commits that change what ships (#2981)
* fix(#2980): pre-skip workflow-file cherry-picks in release-sdk hotfix loop The default GITHUB_TOKEN issued to the release-sdk run lacks the `workflow` scope, so the prepare job's `git push origin "$BRANCH"` is rejected by GitHub when any cherry-picked commit modifies a file under `.github/workflows/`: ! [remote rejected] hotfix/X.YY.Z -> hotfix/X.YY.Z (refusing to allow a GitHub App to create or update workflow ... without `workflows` permission) Pre-#2980 behavior: the auto_cherry_pick loop happily picked workflow-file commits, then the trailing push exploded with no clear signal which commit was the culprit. v1.39.1 hit this on PR #2977 (run 25232010071) — earlier release-sdk fixes (#2965, #2967, #2970) had been skipped on conflict so their workflow-file changes never reached the push step, masking the bug; #2977 was the first workflow-file commit to apply cleanly and the push immediately exploded. Fix: pre-pick guard in the cherry-pick loop. Inspect each candidate commit's file list via `git diff-tree --no-commit-id --name-only -r` BEFORE attempting the pick. If any path matches `^\.github/workflows/`, skip the commit, emit a `::warning::` annotation naming the dropped commit, and append to a new `WORKFLOW_SKIPPED` bucket. The run summary surfaces this bucket in its own section, distinct from `CONFLICT_SKIPPED` (real merge conflicts) and `POLICY_SKIPPED` (feat/refactor exclusions), so operators reviewing the run never confuse the remediation paths. The loud-warning piece is non-negotiable: silent drops were explicitly rejected as a failure mode during the option-1/2/3 tradeoff discussion. If a workflow-file fix genuinely needs to ship in a hotfix, the operator applies it manually on the hotfix branch using a token with `workflow` scope, or lands it on main and re-cuts the release. Regression covered by tests/bug-2980-skip-workflow-file-cherrypicks.test.cjs (5 assertions: pre-pick guard exists, uses `git diff-tree`, emits `::warning::`, lands in dedicated bucket, surfaces in summary). The bug-2964 test's 4 KB window after the cherry-pick-loop anchor was nudged to 6 KB to accommodate the new pre-pick scaffolding — the test's own comment had already anticipated this kind of growth (citing #2970's merge-commit pre-skip as prior precedent). Closes #2980 * refactor(#2980): replace workflow-file pre-skip with shipped-paths filter The previous commit on this branch caught only the .github/workflows/* subset of the bug, treating the symptom (push rejection on workflow-file changes) rather than the root cause (the fix:/chore: filter is too broad — it picks any commit with that conventional-commit type even when the diff cannot affect the published npm package). CI-only fixes (release-sdk.yml itself, hotfix tooling, test-only commits) shouldn't flow through hotfix runs at all — they cannot change what `npm install get-shit-done-cc@X.YY.Z` produces. The .github/workflows/* push rejection is just the loudest of these "shouldn't have been picked" cases; tests/, docs/, .planning/ commits get picked silently with the same lack of effect on consumers. Replace the workflow-file pre-skip with a shipped-paths filter: - New scripts/diff-touches-shipped-paths.cjs reads package.json `files`, plus package.json itself (always-shipped per `npm pack` semantics), and exits 0 iff any input path is in the shipped set. Lockfile is not shipped (npm pack excludes it unless explicitly in `files`). - Workflow loop now pipes `git diff-tree --no-commit-id --name-only -r` through the classifier; on exit 1 the commit is skipped and appended to a new NON_SHIPPED_SKIPPED bucket (replaces WORKFLOW_SKIPPED). - Run summary surfaces NON_SHIPPED_SKIPPED as informational — no ::warning:: annotation. A non-shipping commit cannot affect the package, so a yellow alert would imply remediation is possible and would mislead operators. The classifier in a separate .cjs file (rather than inline bash heredoc) is so its rules — directory-prefix vs exact-match, package.json-always-shipped, lockfile-not-shipped — are unit-testable in tests/bug-2980-hotfix-only-picks-shipping-changes.test.cjs (11 new assertions: 4 static workflow + 6 classifier behavioral + 1 mixed- diff edge case). Why this dissolves the original push-rejection bug: workflow files aren't in `files`, so workflow-only commits are skipped pre-pick. The push step never sees them. If a workflow-file fix genuinely needs to ship in a hotfix release (extremely rare — the hotfix workflow is read from main's ref, not the hotfix branch's), the operator applies it manually using a token with `workflow` scope. The pre-skip puts that requirement in the run summary explicitly. Closes #2980 |
||
|
|
7a416b10d4 |
fix(#2976): allow same-version bump in release-sdk hotfix release job (#2977)
The release job's "Bump in-tree version (not committed)" step ran `npm version "$VERSION" --no-git-tag-version` without --allow-same-version, so on real hotfix runs it failed with `npm error Version not changed` — because the prepare job had already committed the bump on the hotfix branch (the release job checks out BRANCH on real runs vs BASE_TAG on dry-runs, which is why dry-run never caught it). Pass --allow-same-version to both bumps, matching release.yml:326. Closes #2976 |
||
|
|
b8d9bd69b2 |
fix(release-sdk): skip all cherry-pick conflicts in hotfix loop (full automation) (#2970)
* fix(release-sdk): skip all cherry-pick conflicts in hotfix loop Full-automation policy: any conflict the cherry-pick can't auto-resolve — context-missing (#2966) or real merge conflict — is now skipped, not aborted. The hotfix run completes with whatever applies cleanly; the SKIPPED list in the run summary becomes the operator's post-hoc review queue. Surfaced in run 25227493387 (1.39.1 dry-run): commit |
||
|
|
0d25ef0c47 |
fix(release-sdk): skip cherry-picks whose target context is absent at base (#2967)
* fix(release-sdk): skip cherry-picks whose target context is absent at base
When auto_cherry_pick processed a fix:/chore: commit whose patch modified
code that didn't exist at the hotfix base tag — typically because the
surrounding infrastructure was added later in a feat/refactor commit
excluded by the filter — `git cherry-pick` failed with a conflict that
no operator could meaningfully resolve, and the loop bricked the run.
Discovered re-running the 1.39.1 dry-run after #2965 merged: cherry-pick
of `a3467792` (the #2965 merge itself) failed because the auto_cherry_pick
block it modifies was added in #2956 ("Add automated cherry-pick + SDK-
bundle parity to hotfix flow") — an Add/feat commit, so the fix/chore
filter excludes it. v1.39.0 has no such block, so the patch had no
anchor.
The conflict is unmistakably distinguishable from a real content conflict:
git emits marker blocks where every `<<<<<<< HEAD ... =======` HEAD
section is empty (no anchor lines to reconcile against), while real
conflicts have content on both sides.
After cherry-pick fails:
1. List unmerged paths via `git diff --diff-filter=U`.
2. For each, scan conflict markers with awk. If every HEAD section is
blank/whitespace-only across every block, classify as
context-missing.
3. Context-missing → `git cherry-pick --skip` and append to SKIPPED
list with reason "(context absent at base)".
4. Otherwise fall through to the existing abort/push-partial/error
path that surfaces the conflict for operator resolution.
Real conflicts still surface with the same workflow as before.
Tests in tests/bug-2966-cherry-pick-context-missing.test.cjs cover:
- Static — extracts the "Prepare hotfix branch" run block via
indentation-aware YAML parsing (no raw-text grep) and asserts the
classification predicate, --skip call, and skipped-reason annotation
are present.
- Behavioral — synthetic repo reproducing the real shape of the
failure, asserts cherry-pick exits non-zero and produces the
empty-HEAD marker shape.
- Predicate — pulls the awk script out of the deployed workflow and
feeds it sample conflict shapes (empty-HEAD, real, mixed,
whitespace-only); asserts each is classified as the workflow will
behave.
Local: `node --test tests/bug-2966-...test.cjs` → 3/3 pass.
Local: `npm run lint:tests` → 0 violations.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* fix(release-sdk): pin merge.conflictStyle=merge on hotfix cherry-pick
CodeRabbit flagged on #2967 that the awk classifier introduced for #2966
assumes default conflict-marker style (plain `<<<<<<< HEAD ... ======= ...
>>>>>>>`). If a runner has merge.conflictStyle=diff3 or zdiff3 set
(globally, repo-config, or via git defaults shift), the marker emits an
extra `||||||| ancestor` section between HEAD and =======. The awk's
`in_head` mode would accumulate that ancestor content into the HEAD
buffer, and a context-missing conflict would misclassify as real —
sending the workflow into the abort path on a pick that should be
silently skipped.
Pass `-c merge.conflictStyle=merge` on the cherry-pick command itself
(scoped to that one git invocation; doesn't leak to other commands).
This guarantees marker shape regardless of the runner's git config.
Updated the existing static assertion in
tests/bug-2966-cherry-pick-context-missing.test.cjs to require the pin —
a future edit dropping it fails the test.
Local: `node --test tests/bug-2966-...test.cjs` → 3/3 pass.
https://claude.ai/code/session_01LApueb9PVs2uSBhsLprVzG
* test(#2964): allow git options between `git` and `cherry-pick`
The previous commit on this branch (
|
||
|
|
a346779213 | fix(release-sdk): allow empty/redundant commits during hotfix cherry-pick (#2965) | ||
|
|
53cda93a01 |
Add automated cherry-pick + SDK-bundle parity to hotfix flow (#2956)
* feat(workflows): hotfix auto-cherry-pick + SDK-bundle parity (#2955) hotfix.yml: - create: auto-cherry-picks fix:/chore: commits from origin/main since BASE_TAG, oldest-first. Patch-equivalents skipped via git cherry. feat:/refactor: never auto-included. Conflicts halt with offending SHA. - finalize: install-smoke gate, sdk-bundle/gsd-sdk.tgz parity with release-sdk.yml, tightened next dist-tag re-point, --latest on gh release create. SDK package.json bumped in lockstep. release-sdk.yml: - New action input (publish | hotfix) and auto_cherry_pick boolean. - New prepare job branches hotfix/X.YY.Z from highest vX.YY.* tag, cherry-picks same logic as hotfix.yml, outputs effective ref. - install-smoke and release consume prepare.outputs.ref. - Hotfix mode forces tag=latest, opens merge-back PR. Idempotent if branch already exists. VERSIONING.md: documents the cumulative-tag invariant (vX.YY.Z anchors vX.YY.{Z+1}) and both workflow paths. Closes #2955 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(code-review): wire --fix dispatch and update stale command references (#2947) * fix(#2893): surface non-canonical plan filenames instead of silently returning zero plans Reporter saw `plan_count: 0` from `/gsd:execute-phase` even though five plan files existed on disk. Investigation showed the planner had written files like `01-PLAN-01-foundation.md`, while `phase-plan-index`'s strict filter (`f.endsWith('-PLAN.md') || f === 'PLAN.md'`) rejected them silently — collapsing two distinct states into the same `plans: []` return: - directory truly has no plans (legit empty) - directory has plans but the filter rejected them (user/agent error) The canonical contract is documented in three places: - `agents/gsd-planner.md` write_phase_prompt step (lines 1063-1080) - `commands/gsd/plan-phase.md` - `references/universal-anti-patterns.md` (rule 26) It mandates `{padded_phase}-{NN}-PLAN.md` and explicitly forbids `PLAN-NN.md` / `01-PLAN-01.md` / `plan-NN.md` etc. The strict filter is correct per that contract. The bug is that the executor never tells the user when the contract was violated — they just see `plan_count: 0` with no signal. Fix: add a diagnostic helper `describeNonCanonicalPlans()` that scans the phase directory for files matching `*PLAN*.md` (the diagnostic net) that the canonical filter rejected, excluding legit derivatives like `*-PLAN-OUTLINE.md` and `*-PLAN.pre-bounce.md`. When offenders exist, return a `warning` field naming each one and citing the canonical pattern so the user knows what to rename to. Wired into the three filter sites: - `phase-plan-index` (the executor's main entry point) - `phases list --type plans` - `find-phase` The strict filter itself is unchanged — existing canonical plans behave identically. This is purely a diagnostic that converts silent-empty into loud-with-actionable-error. Tests: - `phase-plan-index returns warning for reporter's exact filename pattern (`01-PLAN-01-foundation.md`)` - `truly empty dir does not emit a warning` - `canonical plans + outline + pre-bounce files do not emit a warning` Closes #2893 * test(#2893): add parity tests for find-phase and phases list --type plans warnings CodeRabbit's only finding on the prior commit: I wired the warning into three filter sites (`phase-plan-index`, `find-phase`, `phases list --type plans`) but only `phase-plan-index` had test coverage for the warning shape. The other two paths could silently diverge during future refactors — exactly the silent-drift class of bug this fix exists to prevent. Add four parity tests mirroring the existing two: - find-phase: non-canonical filenames produce a warning naming each offender + citing the canonical pattern. - find-phase: canonical plan + derivative files (PLAN-OUTLINE, pre-bounce) produce no warning. - phases list --type plans: same non-canonical case, but assert the warning is prefixed with `${dir}: ` (this path aggregates across phase directories so each offender is tagged with its dir). - phases list --type plans: canonical case, no warning. `node --test tests/phase.test.cjs`: 98/98 pass (was 94, +4 new). * docs(changelog): hotfix flow auto-cherry-pick + SDK bundle parity (#2955) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(workflows): address CodeRabbit findings on hotfix flow (#2955) 5 findings, all real: 1. BASE_TAG selection used lexicographic awk compare, breaking on multi-digit patches (v1.27.10 wrongly < v1.27.2). Fixed in both hotfix.yml and release-sdk.yml: append TARGET_TAG to candidate list, sort -V, take preceding entry. Semver-correct. 2,4. Cherry-pick conflict aborted locally with no remote branch to resolve from. Now the skeleton branch is pushed up-front (real runs); on conflict we abort, push the partial-pick state with --force-with-lease, and emit operator instructions in the run summary. 3. release-sdk.yml dry_run exited before cherry-pick, defeating the purpose. Now dry_run still applies cherry-picks locally (catches conflicts), just skips push. Downstream install-smoke runs against BASE_TAG; the cherry-pick verification itself is the dry-run signal. 5. release-sdk.yml release job missing pull-requests: write — gh pr create for the merge-back PR would have failed under restricted token defaults. Permission added. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(workflows): CR round 2 — dry-run signal + post-publish reconciliation (#2955) 3 findings, all real: 6. hotfix.yml create dry_run skipped every step (branch creation, cherry-pick, version bump) — a green dry-run gave no signal at all. Now the local checkout/cherry-pick/bump always runs; only the git push calls are gated on dry_run. Conflicts surface in dry-run too. 7,8. "Refuse if version already on npm" preflight hard-failed reruns, so a transient failure between npm publish and a later step (tag push, GH release, merge-back PR, dist-tag re-point) left the release half-shipped with no path to reconcile. Replaced with a prior_publish detect step that warns and sets skip_publish=true; the publish step is gated on that flag, but tag/release/PR/dist-tag continue. GitHub Release create is now idempotent (edit --latest if already exists). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(workflows): CR round 3 — preserve dry-run cherry-pick history in conflict guidance (#2955) Dry-run conflict path discarded successful picks with the runner, but the message told operators to rerun with auto_cherry_pick=false — which recreates the branch from BASE_TAG and silently loses every pick that had succeeded before the conflict. Updated both hotfix.yml and release-sdk.yml: dry-run conflict summary now lists the lost SHAs and recommends re-running with auto_cherry_pick=true (real, not dry-run) to materialize the partial branch on origin. Real-run guidance unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
0e0f6952c5 |
ci(release-sdk): bring CI gates to parity with release.yml (#2929)
Ports the pre-publish CI gates that release.yml applies into release-sdk.yml,
so the stopgap workflow ships releases at the same quality bar as the
canonical workflow (minus the @gsd-build/sdk publish, still intentionally
omitted, and the release-branch ceremony, intentionally omitted).
Changes (all mechanical copies of release.yml patterns):
- install-smoke as needs: dependency. The reusable workflow at
.github/workflows/install-smoke.yml runs the cross-platform install
matrix (Ubuntu 22/24, macOS 24, packed-vs-unpacked). Publish job
won't start until install-smoke passes for the dispatched ref.
- npm test → npm run test:coverage. Full coverage gate, matching
release.yml's pre-publish test step.
- Tolerant tag-existence check. The previous upfront "refuse if tag
exists" was too strict — operators re-running after a mid-flight
publish-step failure would be blocked by the tag they successfully
pushed last time. New behavior matches release.yml: skip the tag
step if the tag points at HEAD; error only if it points elsewhere.
- Tag-and-push step gets the same skip-if-at-HEAD pattern.
- New "Re-point next dist-tag at the new latest" step, gated on
tag=latest. Matches release.yml#finalize "Clean up next dist-tag" —
keeps @next from going stale relative to @latest.
- New "Create GitHub Release" step. Per-tag flag selection:
tag=dev, tag=next → --prerelease (won't be highlighted on repo home)
tag=latest → --latest (becomes the highlighted release)
All use --generate-notes so the release body auto-fills from commits.
- Summary updated to mention the GitHub Release and dist-tag re-point.
Out of scope per #2929:
- canary.yml, release.yml unchanged (verified by file diff)
- bin/install.js unchanged (install path already uses bundled SDK)
- No @gsd-build/sdk publish anywhere
- No release/X.Y.Z branch ceremony (this stopgap targets dispatched
ref directly)
|
||
|
|
e107bb35d4 |
feat(ci): add release-sdk.yml stopgap workflow for dev/next/latest CC publishes (#2925)
Adds a workflow_dispatch-only release path that publishes get-shit-done-cc to ONE chosen dist-tag per run (dev | next | latest), with the SDK bundled inside the CC tarball both as the existing loose sdk/dist/ tree and as a fresh sdk-bundle/gsd-sdk.tgz npm-installable artifact. Why: @gsd-build/sdk publishes from canary.yml and release.yml fail because the @gsd-build npm token is currently unavailable. CC users don't consume @gsd-build/sdk directly — bin/gsd-sdk.js resolves sdk/dist/cli.js from inside the installed CC package. This workflow ships only get-shit-done-cc (which we hold the token for) and bundles the SDK two ways so any future install path can pick whichever shape it needs. The new sdk-bundle/ directory is added to the CC files whitelist in-tree at build time only — never committed. Existing canary.yml and release.yml are intentionally untouched; restore them to primary use once the @gsd-build/sdk token is recovered. Per-tag version derivation when the version input is empty: - dev → <base>-dev.N (next sequential, scanning v<base>-dev.* tags) - next → <base>-rc.N (matches release.yml convention) - latest → <base> (clean, no suffix) Refuses to publish when the version already exists on npm or has an existing git tag (no accidental overwrites). Verifies the publish landed on the registry and the dist-tag resolves correctly before marking the run successful. |
||
|
|
006cdafe8f |
ci(drift): enforce alias freshness checks in CI and contributor flow (#2910)
Merging alias-drift guardrails and local hook hardening. |
||
|
|
f2ada8500c |
chore(#2868): switch canary publish from main to dev branch (#2871)
* chore(#2868): switch canary publish from main to dev branch Swaps the four `if:` guards in `.github/workflows/canary.yml` from `refs/heads/main` to `refs/heads/dev` so the canary stream is owned by the new long-lived integration branch. Adds a policy comment at the top of the workflow documenting the branch->dist-tag mapping (dev=@canary, main=@next/@latest, no overlap). Closes #2868 * fix(#2868): summary block matches publish-step gate CodeRabbit caught: the Summary step keyed off DRY_RUN only, so a non-dry-run on main would falsely report "Published"/"Tagged" even though all four publish steps were skipped by the new dev-only gate. Add PUBLISH_ELIGIBLE env mirroring the publish-step `if:` expression and a VALIDATION ONLY branch in the summary so non-dev runs report honestly. |
||
|
|
f6a6e43226 |
fix(#2872): auto-close PRs that omit the issue-link keyword (#2873)
The Require Issue Link workflow was posting a comment and failing the status check, but never transitioning the PR to closed. PR templates promise auto-close behavior; PR #2863 demonstrated the gap (opened without a Closes #N, sat open until manually closed). Adds a `pulls.update({state: 'closed'})` call after the existing comment, updates the comment heading to 'PR auto-closed', and tells the author how to reopen after fixing the body. Closes #2872 |
||
|
|
91194cdbff |
chore(#2828): add canary release workflow (#2830)
* chore(#2828): add canary release workflow (dev builds on push to main) Publishes get-shit-done-cc@canary and @gsd-build/sdk@canary on every push to main. Version format: {base}-canary.{N} where base strips any pre-release suffix from package.json (1.39.0-rc.4 → 1.39.0-canary.1). Sequential canary number is auto-detected from existing git tags so reruns never collide. Concurrency group cancels stale in-flight canary runs when commits land quickly. Mirrors the structure and steps of release.yml: same checkout pins, Node 24, npm-publish environment, build:sdk, tarball verification, dry-run publish gate, and publish verification with sleep 10. Closes #2828 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(#2828): address CodeRabbit review findings on canary.yml - cancel-in-progress: false — was true, allowing a newer push to cancel a run mid-publish (after tag push but before SDK publish), leaving a partial release state that's unrecoverable since npm versions are immutable - Guard tag/publish/verify steps with github.ref == 'refs/heads/main' so a manual workflow_dispatch from a feature branch (dry_run defaults false) cannot accidentally publish unmerged code under the shared canary dist-tag - Replace fixed sleep 10 with exponential backoff retry loop (delays: 5 10 20 30 45s); fixed sleep is flaky against normal npm CDN replication lag and a false failure forces a new canary number since the tag already exists Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(plan-phase): expose --mvp flag in command frontmatter Adds --mvp to argument-hint and Flags doc. Workflow handler in next commit. * chore(#2828): remove push:main trigger from canary workflow Submission rate to main is too high to auto-publish a canary on every merge. Restrict the workflow to manual workflow_dispatch only. Closes #2828 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
aeef87de7f |
docs(test-standards): enforce no-source-grep rule with CI linter + CONTRIBUTING.md (#2700)
* docs(test-standards): enforce no-source-grep rule with CI linter + update CONTRIBUTING.md
Adds scripts/lint-no-source-grep.cjs — a static linter that detects readFileSync
on .cjs source files in tests without an allow-test-rule annotation. Wires it
into CI as a new lint-tests job in test.yml and as npm run lint:tests.
Resolves all 9 existing violations across the test suite:
- Rewrites workspace routing tests (3) as behavioral runGsdTools calls that
verify each command is router-recognized (exit != "Unknown init workflow")
- Adds allow-test-rule annotations with explanatory comments to 7 legitimate
structural tests: architectural invariants (locking, orphan-worktree),
structural regression guards (milestone-regex-global), docs-parity
(config-field-docs), integration-test-input (copilot-install), and
structural-implementation-guards (bug-1891, discuss-mode)
Updates CONTRIBUTING.md Testing Standards section with:
- "Prohibited: Source-Grep Tests" section with the before/after pattern,
root cause analysis of why it breaks (commit
|
||
|
|
259c1d07d3 |
fix(#2647): guard tarball ships sdk/dist so gsd-sdk query works (#2671)
v1.38.3 shipped without sdk/dist/ because the outer `files` whitelist and `prepublishOnly` chain had drifted. The `gsd-sdk` bin shim then fell through to a stale @gsd-build/sdk@0.1.0 (pre-`query`), breaking every workflow that called `gsd-sdk query <noun>` on fresh installs. Current package.json already restores `sdk/dist` + `build:sdk` prepublish; this PR locks the fix in with: - tests/bug-2647-outer-tarball-sdk-dist.test.cjs — asserts `files` includes `sdk/dist`, `prepublishOnly` invokes `build:sdk`, the shim resolves sdk/dist/cli.js, `npm pack --dry-run` lists sdk/dist/cli.js, and the built CLI exposes a `query` subcommand. - scripts/verify-tarball-sdk-dist.sh — packs, extracts, installs prod deps, and runs `node sdk/dist/cli.js query --help` against the real tarball output. - .github/workflows/release.yml — runs the verify script in both next and stable release jobs before `npm publish`. Partial fix for #2649 (same root cause on the sibling sdk package). Fixes #2647 |
||
|
|
31569c8cc8 |
ci: explicit rebase check + fail-fast SDK typecheck in install-smoke (#2631)
* ci: explicit rebase check + fail-fast SDK typecheck in install-smoke Stale-base regression guard. Root cause: GitHub's `refs/pull/N/merge` is cached against the PR's recorded merge-base, not current main. When main advances after a PR is opened, the cache stays stale and CI runs against the pre-advance tree. PRs hit this whenever a type error lands on main and gets patched shortly after (e.g. #2611 + #2622) — stale branches replay the broken intermediate state and report confusing downstream failures for hours. Observed failure mode: install-smoke's "Assert gsd-sdk resolves on PATH" step fires with "installSdkIfNeeded() regression" even when the real cause is `npm run build` failing in sdk/ due to a TypeScript cast mismatch already fixed on main. Fix: - Explicit `git merge origin/main` step in both `install-smoke.yml` and `test.yml`. If the merge conflicts, emit a clear "rebase onto main" diagnostic and fail early, rather than let conflicts produce unrelated downstream errors. - Dedicated `npm run build:sdk` typecheck step in install-smoke with a remediation hint ("rebase onto main — the error may already be fixed on trunk"). Fails fast with the actual tsc output instead of masking it behind a PATH assertion. - Drop the `|| true` on `get-shit-done-cc --claude --local` so installer failures surface at the install step with install.js's own error message, not at the downstream PATH assertion where the message misleadingly blames "shim regression". - `fetch-depth: 0` on checkout so the merge-base check has history. * ci: address CodeRabbit — add rebase check to smoke-unpacked, fix fetch flag Two findings from CodeRabbit's review on #2631: 1. `smoke-unpacked` job was missing the same rebase check applied to the `smoke` job. It ran on the cached `refs/pull/N/merge` and could hit the same stale-base failure mode the PR was designed to prevent. Added the identical rebase-check step. 2. `git fetch origin main --depth=0` is an invalid flag — git rejects it with "depth 0 is not a positive number". The intent was "fetch with full depth", but the right way is just `git fetch origin main` (no --depth). Removed the invalid flag and the `||` fallback that was papering over the error. |
||
|
|
0a049149e1 |
fix(sdk): decouple from build-from-source install, close #2441 #2453 (#2457)
* fix(sdk): decouple SDK from build-from-source install path, close #2441 and #2453 Ship sdk/dist prebuilt in the tarball and replace the npm-install-g sub-install with a parent-package bin shim (bin/gsd-sdk.js). npm chmods bin entries from a packed tarball correctly, eliminating the mode-644 failure (#2453) and the full class of NPM_CONFIG_PREFIX/ignore-scripts/ corepack/air-gapped failure modes that caused #2439 and #2441. Changes: - sdk/package.json: prepublishOnly runs `rm -rf dist && tsc && chmod +x dist/cli.js` (stale-build guard + execute-bit fix at publish time) - package.json: add "gsd-sdk": "bin/gsd-sdk.js" bin entry; add sdk/dist to files so the prebuilt CLI ships in the tarball - bin/gsd-sdk.js: new back-compat shim — resolves sdk/dist/cli.js relative to the package root and delegates via `node`, so all existing PATH call sites (slash commands, agents, hooks) continue to work unchanged (S1 shim) - bin/install.js: replace installSdkIfNeeded() build-from-source + global- install dance with a dist-verify + chmod-in-place guard; delete resolveGsdSdk(), detectShellRc(), emitSdkFatal() helpers now unused - .github/workflows/install-smoke.yml: add smoke-unpacked job that strips execute bit from sdk/dist/cli.js before install to reproduce the exact #2453 failure mode - tests/bug-2441-sdk-decouple.test.cjs: new regression tests asserting all invariants (no npm install -g from sdk/, shim exists, sdk/dist in files, prepublishOnly has rm -rf + chmod) - tests/bugs-1656-1657.test.cjs: update stale assertions that required build-from-source behavior (now asserts new prebuilt-dist invariants) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore(release): bump to 1.38.2, wire release.yml to build SDK dist - Bump version 1.38.1 -> 1.38.2 for the #2441/#2453 fix shipped in 0f6903d. - Add `build:sdk` script (`cd sdk && npm ci && npm run build`). - `prepublishOnly` now runs hooks + SDK builds as a safety net. - release.yml (rc + finalize): build SDK dist before `npm publish` so the published tarball always ships fresh `sdk/dist/` (kept gitignored). - CHANGELOG: document 1.38.2 entry and `--sdk` flag semantics change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: build SDK dist before tests and smoke jobs sdk/dist/ is gitignored (built fresh at publish time via release.yml), but both the test suite and install-smoke jobs run `bin/install.js` or `npm pack` against the checked-out tree where dist doesn't exist yet. - test.yml: `npm run build:sdk` before `npm run test:coverage`, so tests that spawn `bin/install.js` don't hit `installSdkIfNeeded()`'s fatal missing-dist check. - install-smoke.yml (both smoke and smoke-unpacked): build SDK before pack/chmod so the published tarball contains dist and the unpacked install has a file to strip exec-bit from. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sdk): lift SDK runtime deps to parent so tarball install can resolve them The SDK's runtime deps (ws, @anthropic-ai/claude-agent-sdk) live in sdk/package.json, but sdk/node_modules is NOT shipped in the parent tarball — only sdk/dist, sdk/src, sdk/prompts, and sdk/package.json are. When a user runs `npm install -g get-shit-done-cc`, npm installs the parent's node_modules but never runs `npm install` inside the nested sdk/ directory. Result: `node sdk/dist/cli.js` fails with ERR_MODULE_NOT_FOUND for 'ws'. The smoke tarball job caught this; the unpacked variant masked it because `npm install -g <dir>` copies the entire workspace including sdk/node_modules (left over from `npm run build:sdk`). Fix: declare the same deps in the parent package.json so they land in <pkg>/node_modules, which Node's resolution walks up to from <pkg>/sdk/dist/cli.js. Keep them declared in sdk/package.json too so the SDK remains a self-contained package for standalone dev. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(lockfile): regenerate package-lock.json cleanly The previous `npm install` run left the lockfile internally inconsistent (resolved esbuild@0.27.7 referenced but not fully written), causing `npm ci` to fail in CI with "Missing from lock file" errors. Clean regen via rm + npm install fixes all three failed jobs (test, smoke, smoke-unpacked), which were all hitting the same `npm ci` sync check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(deps): remove unused esbuild + vitest from root devDependencies Both were declared but never imported anywhere in the root package (confirmed via grep of bin/, scripts/, tests/). They lived in sdk/ already, which is the only place they're actually used. The transitive tree they pulled in (vitest → vite → esbuild 0.28 → @esbuild/openharmony-arm64) was the root of the CI npm ci failures: the openharmony platform package's `optional: true` flag was not being applied correctly by npm 10 on Linux runners, causing EBADPLATFORM. After removal: 800+ transitive packages → 155. Lockfile regenerated cleanly. All 4170 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sdk): pretest:coverage builds sdk; tighten shim test assertions Add "pretest:coverage": "npm run build:sdk" so npm run test:coverage works in clean checkouts where sdk/dist/ hasn't been built yet. Tighten the two loose shim assertions in bug-2441-sdk-decouple.test.cjs: - forwards-to test now asserts path.resolve() is called with the 'sdk','dist','cli.js' path segments, not just substring presence - node-invocation test now asserts spawnSync(process.execPath, [...]) pattern, ruling out matches in comments or the shebang line Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address PR review — pretest:coverage + tighten shim tests Review feedback from trek-e on PR 2457: 1. pretest:coverage + pretest hooks now run `npm run build:sdk` so `npm run test[:coverage]` in a clean checkout produces the required sdk/dist/ artifacts before running the installer-dependent tests. CI already does this explicitly; local contributors benefit. 2. Shim tests in bug-2441-sdk-decouple.test.cjs tightened from loose substring matches (which would pass on comments/shebangs alone) to regex assertions on the actual path.resolve call, spawnSync with process.execPath, process.argv.slice(2), and process.exit pattern. These now provide real regression protection for #2453-class bugs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: correct CHANGELOG entry and add [1.38.2] reference link Two issues in the 1.38.2 CHANGELOG entry: - installSdkIfNeeded() was described as deleted but it still exists in bin/install.js (repurposed to verify sdk/dist/cli.js and fix execute bit). Corrected the description to say 'repurposes' rather than 'deletes'. - The reference-link block at the bottom of the file was missing a [1.38.2] compare URL and [Unreleased] still pointed to v1.37.1...HEAD. Added the [1.38.2] link and updated [Unreleased] to compare/v1.38.2...HEAD. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(sdk): double-cast WorkflowConfig to Record for strict tsc build TypeScript error on main (introduced in #2611) blocks `npm run build` in sdk/, which now runs as part of this PR's tarball build path. Apply the double-cast via `unknown` as the compiler suggests. Same fix as #2622; can be dropped if that lands first. * test: remove bug-2598 test obsoleted by SDK decoupling The bug-2598 test guards the Windows CVE-2024-27980 fix in the old build-from-source path (npm spawnSync with shell:true + formatSpawnFailure diagnostics). This PR removes that entire code path — installSdkIfNeeded no longer spawns npm, it just verifies the prebuilt sdk/dist/cli.js shipped in the tarball. The test asserts `installSdkIfNeeded.toString()` contains a formatSpawnFailure helper. After decoupling, no such helper exists (nothing to format — there's no spawn). Keeping the test would assert invariants of the rejected architecture. The original #2598 defect (silent failure of npm spawn on Windows) is structurally impossible in the shim path: bin/gsd-sdk.js invokes `node sdk/dist/cli.js` directly via child_process.spawn with an explicit argv array. No .cmd wrapper, no shell delegation. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Tom Boucher <trekkie@nomorestars.com> |
||
|
|
43ea92578b |
Merge remote-tracking branch 'origin/main' into hotfix/1.38.2
# Conflicts: # CHANGELOG.md # bin/install.js # sdk/src/query/init.ts |
||
|
|
ebbe74de72 |
feat(release): publish @gsd-build/sdk alongside get-shit-done-cc in release pipeline (#2468)
* fix(sdk): bump engines.node from >=20 to >=22.0.0 Node 20 reaches EOL April 30 2026. The root package already declares >=22.0.0 and CI only runs Node 22 and 24. Align sdk/package.json so `npm install` on Node 20 fails with a clear engines mismatch rather than a silent install that breaks at runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(release): publish @gsd-build/sdk alongside get-shit-done-cc in release pipeline Closes #2309 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
f98ef1e460 |
fix(install): fatal SDK install failures + CI smoke gate (#2439)
## Why #2386 added `installSdkIfNeeded()` to build @gsd-build/sdk from bundled source and `npm install -g .`, because the npm-published @gsd-build/sdk is intentionally frozen and version-mismatched with get-shit-done-cc. But every failure path in that function was warning-only — including the final `which gsd-sdk` verification. When npm's global bin is off a user's PATH (common on macOS), the installer printed a yellow warning then exited 0. Users saw "install complete" and then every `/gsd-*` command crashed with `command not found: gsd-sdk` (the #2439 symptom). No CI job executed the install path, so this class of regression could ship undetected — existing "install" tests only read bin/install.js as a string. ## What changed **bin/install.js — installSdkIfNeeded() is now transactional** - All build/install failures exit non-zero (not just warn). - Post-install `which gsd-sdk` check is fatal: if the binary landed globally but is off PATH, we exit 1 with a red banner showing the resolved npm bin dir, the user's shell, the target rc file, and the exact `export PATH=…` line to add. - Escape hatch: `GSD_ALLOW_OFF_PATH=1` downgrades off-PATH to exit 2 for users with intentionally restricted PATH who will wire up the binary manually. - Resolver uses POSIX `command -v` via `sh -c` (replaces `which`) so behavior is consistent across sh/bash/zsh/fish. - Factored `resolveGsdSdk()`, `detectShellRc()`, `emitSdkFatal()`. **.github/workflows/install-smoke.yml (new)** - Executes the real install path: `npm pack` → `npm install -g <tgz>` → run installer non-interactively → `command -v gsd-sdk` → run `gsd-sdk --version`. - PRs: path-filtered to installer-adjacent files, ubuntu + Node 22 only. - main/release branches: full matrix (ubuntu+macos × Node 22+24). - Reusable via workflow_call with `ref` input for release gating. **.github/workflows/release.yml — pre-publish gate** - New `install-smoke-rc` and `install-smoke-finalize` jobs invoke the reusable workflow against the release branch. `rc` and `finalize` now `needs: [validate-version, install-smoke-*]`, so a broken SDK install blocks `npm publish`. ## Test plan - Local full suite: 4154/4154 pass - install-smoke.yml will self-validate on this PR (ubuntu+Node22 only) Addresses root cause of #2439 (the per-command pre-flight in #2440 is the complementary defensive layer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
af66cd89ca |
fix(install): fatal SDK install failures + CI smoke gate (#2439)
## Why #2386 added `installSdkIfNeeded()` to build @gsd-build/sdk from bundled source and `npm install -g .`, because the npm-published @gsd-build/sdk is intentionally frozen and version-mismatched with get-shit-done-cc. But every failure path in that function was warning-only — including the final `which gsd-sdk` verification. When npm's global bin is off a user's PATH (common on macOS), the installer printed a yellow warning then exited 0. Users saw "install complete" and then every `/gsd-*` command crashed with `command not found: gsd-sdk` (the #2439 symptom). No CI job executed the install path, so this class of regression could ship undetected — existing "install" tests only read bin/install.js as a string. ## What changed **bin/install.js — installSdkIfNeeded() is now transactional** - All build/install failures exit non-zero (not just warn). - Post-install `which gsd-sdk` check is fatal: if the binary landed globally but is off PATH, we exit 1 with a red banner showing the resolved npm bin dir, the user's shell, the target rc file, and the exact `export PATH=…` line to add. - Escape hatch: `GSD_ALLOW_OFF_PATH=1` downgrades off-PATH to exit 2 for users with intentionally restricted PATH who will wire up the binary manually. - Resolver uses POSIX `command -v` via `sh -c` (replaces `which`) so behavior is consistent across sh/bash/zsh/fish. - Factored `resolveGsdSdk()`, `detectShellRc()`, `emitSdkFatal()`. **.github/workflows/install-smoke.yml (new)** - Executes the real install path: `npm pack` → `npm install -g <tgz>` → run installer non-interactively → `command -v gsd-sdk` → run `gsd-sdk --version`. - PRs: path-filtered to installer-adjacent files, ubuntu + Node 22 only. - main/release branches: full matrix (ubuntu+macos × Node 22+24). - Reusable via workflow_call with `ref` input for release gating. **.github/workflows/release.yml — pre-publish gate** - New `install-smoke-rc` and `install-smoke-finalize` jobs invoke the reusable workflow against the release branch. `rc` and `finalize` now `needs: [validate-version, install-smoke-*]`, so a broken SDK install blocks `npm publish`. ## Test plan - Local full suite: 4154/4154 pass - install-smoke.yml will self-validate on this PR (ubuntu+Node22 only) Addresses root cause of #2439 (the per-command pre-flight in #2440 is the complementary defensive layer). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e3bd06c9fd |
fix(release): make merge-back PR step non-fatal
Repos that disable "Allow GitHub Actions to create and approve pull requests" (org-level policy or repo-level setting) cause the "Create PR to merge release back to main" step to fail with a GraphQL 403. That failure cascades: Tag and push, npm publish, GitHub Release creation are all skipped, and the entire release aborts. The merge-back PR is a convenience — it's re-openable manually after the release. Making it non-fatal with continue-on-error lets the rest of the release complete. The step now emits ::warning:: annotations pointing at the manual-recovery command when it fails. Shell pipelines also fall through with `|| echo "::warning::..."` so transient gh CLI failures don't mask the underlying policy issue. Covers the failure mode seen on run 24596079637 where dry-run publish validation passed but the release halted at the PR-creation step. |
||
|
|
553d9db56e |
ci: upgrade GitHub Actions to Node 22+ runtimes (#2128)
- actions/checkout v4.2.2 → v6.0.2 (pr-gate, auto-branch) - actions/github-script v7.0.1/v8 → v9.0.0 (all workflows) - actions/stale v9.0.0 → v10.2.0 Eliminates Node.js 20 deprecation warnings. Node 20 actions will be forced to Node 24 on June 2, 2026 and removed Sept 16, 2026. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
6b7b6a0ae8 |
ci: fix release pipeline — update actions, add GH releases, extend CI triggers (#1956)
- Update actions/checkout and actions/setup-node to v6 in release.yml and hotfix.yml (Node.js 24 compat, prevents June 2026 breakage) - Add GitHub Release creation to release finalize, release RC, and hotfix finalize steps (populates Releases page automatically) - Extend test.yml push triggers to release/** and hotfix/** branches - Extend security-scan.yml PR triggers to release/** and hotfix/** branches Closes #1955 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
177cb544cb |
chore(ci): add branch-cleanup workflow — auto-delete on merge + weekly sweep (#2051)
Adds .github/workflows/branch-cleanup.yml with two jobs: - delete-merged-branch: fires on pull_request closed+merged, immediately deletes the head branch. Belt-and-suspenders alongside the repo's delete_branch_on_merge setting (see issue for the one-line owner action). - sweep-orphaned-branches: runs weekly (Sunday 4am UTC) and on workflow_dispatch. Paginates all branches, deletes any whose only closed PRs are merged — cleans up branches that pre-date the setting change. Both jobs use the pinned actions/github-script hash already used across the repo. Protected branches (main, develop, release) are never touched. 422 responses (branch already gone) are treated as success. Closes #2050 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
bad9c63fcb |
ci: update action versions to v6 and extend CI to release/hotfix branches (#1955) (#1965)
- Update actions/checkout from v4.2.2 to v6.0.2 in release.yml and hotfix.yml (prevents breakage after June 2026 Node.js 20 deprecation) - Update actions/setup-node from v4.1.0 to v6.3.0 in both workflows - Add release/** and hotfix/** to test.yml push triggers - Add release/** and hotfix/** to security-scan.yml PR triggers test.yml already used v6 pins — this aligns the release pipelines. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
c7de05e48f |
fix(engines): lower Node.js minimum to 22
Node 22 is still in Active LTS until October 2026 and Maintenance LTS until April 2027. Raising the engines floor to >=24.0.0 unnecessarily locked out a fully-supported LTS version and produced EBADENGINE warnings on install. Restore Node 22 support, add Node 22 to the CI matrix, and update CONTRIBUTING.md to match. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
f7d4d60522 |
fix(ci): drop Node 22 from matrix, require Node 24 minimum (#1848)
Node 20 reached EOL April 30 2026. Node 22 is no longer the LTS baseline — Node 24 is the current Active LTS. Update CI matrix to run only Node 24, raise engines floor to >=24.0.0, and update CONTRIBUTING.md node compatibility table accordingly. Fixes #1847 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
85316d62d5 |
feat: 3-tier release strategy with hotfix, release, and CI workflows (#1289)
* feat: 3-tier release strategy with hotfix, release, and CI workflows Supersedes PRs #1208 and #1210 with a consolidated approach: - VERSIONING.md: Strategy document with 3 release tiers (patch/minor/major) - hotfix.yml: Emergency patch releases to latest - release.yml: Standard release cycle with RC/beta pre-releases to next - auto-branch.yml: Create branches from issue labels - branch-naming.yml: Convention validation (advisory) - pr-gate.yml: PR size analysis and labeling - stale.yml: Weekly cleanup of inactive issues/PRs - dependabot.yml: Automated dependency updates npm dist-tags: latest (stable) and next (pre-release) only, following Angular/Next.js convention. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review findings for release workflow security and correctness - Move all ${{ }} expression interpolation from run: blocks into env: mappings in both hotfix.yml (~12 instances) and release.yml (~16 instances) to prevent potential command injection via GitHub Actions expression evaluation - Reorder rc job in release.yml to run npm ci and test:coverage before pushing the git tag, preventing broken tagged commits when tests fail - Update VERSIONING.md to accurately describe the implementation: major releases use beta pre-releases only, minor releases use rc pre-releases only (no beta-then-rc progression) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * security: harden release workflows — SHA pinning, provenance, dry-run guards Addresses deep adversarial review + best practices research: HIGH: - Fix release.yml rc/finalize: dry_run now gates tag+push (not just npm publish) - Fix hotfix.yml finalize: reorder tag-before-publish (was publish-before-tag) MEDIUM — Security hardening: - Pin ALL actions to SHA hashes (actions/checkout@11bd7190, actions/setup-node@39370e39, actions/github-script@60a0d830) - Add --provenance --access public to all npm publish commands - Add id-token: write permission for npm provenance OIDC - Add concurrency groups (cancel-in-progress: false) on both workflows - Add branch-naming.yml permissions: {} (deny-all default) - Scope permissions per-job instead of workflow-level where possible MEDIUM — Reliability: - Add post-publish verification (npm view + dist-tag check) after every publish - Add npm publish --dry-run validation step before actual publish - Add branch existence pre-flight check in create jobs LOW: - Fix VERSIONING.md Semver Rules: MINOR = "enhancements" not "new features" (aligns with Release Tiers table) Tests: 1166/1166 pass Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * security: pin actions/stale to SHA hash Last remaining action using a mutable version tag. Now all actions across all workflow files are pinned to immutable SHA hashes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address all Copilot review findings on release strategy workflows - Configure git identity in all committing jobs (hotfix + release) - Base hotfix on latest patch tag instead of vX.Y.0 - Add issues: write permission for PR size labeling - Remove stale size labels before adding new one - Make tagging and PR creation idempotent for reruns - Run dry-run publish validation unconditionally - Paginate listFiles for large PRs - Fix VERSIONING.md table formatting and docs accuracy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: clean up next dist-tag after finalize in release and hotfix workflows After finalizing a release, the next dist-tag was left pointing at the last RC pre-release. Anyone running npm install @next would get a stale version older than @latest. Now both workflows point next to the stable release after finalize, matching Angular/Next.js convention. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ci): address blocking issues in 3-tier release workflows - Move back-merge PR creation before npm publish in hotfix/release finalize - Move version bump commit after test step in rc workflow - Gate hotfix create branch push behind dry_run check - Add confirmed-bug and confirmed to stale.yml exempt labels - Fix auto-branch priority: critical prefix collision with hotfix/ naming Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
17c65424ad |
ci: auto-close draft PRs with policy message (#1765)
- Add close-draft-prs.yml workflow that auto-closes draft PRs with explanatory comment directing contributors to submit completed PRs - Update CONTRIBUTING.md with "No draft PRs" policy - Update default PR template with draft PR warning Closes #1762 Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
a6457a7688 |
ci: drop Windows runner, add static hardcoded-path detection (#1676)
Replace the Windows CI runner with a static analysis test that catches the same class of platform-specific path bugs (C:\, /home/, /Users/, /tmp/) without requiring an actual Windows machine. - tests/hardcoded-paths.test.cjs: new static scanner that checks string literals in all source JS/CJS files for hardcoded platform paths; runs on Linux/macOS in <100ms and fires on every PR - .github/workflows/test.yml: remove windows-latest from matrix; switch macOS smoke-test runner from Node 22 → Node 24 (the declared standard) - package.json: bump engines.node from >=20.0.0 to >=22.0.0 (Node 20 reached EOL April 2026) Matrix goes from 4 runners → 3 runners per run: ubuntu/22 ubuntu/24 macos/24 Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
65abc1e685 |
chore: require issue link on all PRs
- PR template: move "Closes #" to top as required field with explicit warning that PRs without a linked issue are closed without review - CONTRIBUTING.md: add mandatory issue-first policy with clear rationale - Add require-issue-link.yml workflow: checks PR body for a closing keyword (Closes/Fixes/Resolves #NNN) on open/edit/reopen/sync events; posts a comment and fails CI if no reference is found PR body is bound to an env var before shell use (injection-safe). The github-script step uses the API SDK, not shell interpolation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
feec5a37a2 |
ci(security): add prompt injection, base64, and secret scanning
Add CI security pipeline to catch prompt injection attacks, base64-obfuscated payloads, leaked secrets, and .planning/ directory commits in PRs. This is critical for get-shit-done because the entire codebase is markdown prompts — a prompt injection in a workflow file IS the attack surface. New files: - scripts/prompt-injection-scan.sh: scans for instruction override, role manipulation, system boundary injection, DAN/jailbreak, and tool call injection patterns in changed files - scripts/base64-scan.sh: extracts base64 blobs >= 40 chars, decodes them, and checks decoded content against injection patterns (skips data URIs and binary content) - scripts/secret-scan.sh: detects AWS keys, OpenAI/Anthropic keys, GitHub PATs, Stripe keys, private key headers, and generic credential patterns - .github/workflows/security-scan.yml: runs all three scans plus a .planning/ directory check on every PR - .base64scanignore / .secretscanignore: per-repo false positive allowlists - tests/security-scan.test.cjs: 51 tests covering script existence, pattern matching, false positive avoidance, and workflow structure All scripts support --diff (CI), --file, and --dir modes. Cross-platform (macOS + Linux). SHA-pinned actions. Environment variables used for github context in run blocks (no direct interpolation). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
57c8a1abbb |
ci: optimize test matrix — 9 containers down to 4
- Drop Node 20 (EOL April 2026)
- Reduce macOS to single runner (Node 22) — platform compat check
- Reduce Windows to single runner (Node 22) — slowest CI, smoke-test
- Keep Ubuntu × {22, 24} as primary test surface
Estimated savings: ~60% fewer runner-minutes per CI run
(~500s → ~190s, 9 jobs → 4 jobs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
d673283cb1 |
Upgrade GitHub Actions for Node 24 compatibility
Signed-off-by: Salman Muin Kayser Chishti <13schishti@gmail.com> |
||
|
|
f656dcbd6f |
chore: update CI matrix to Node 20, 22, 24 — drop EOL Node 18
Node 18 reached EOL April 2025. Node 24 is the current LTS target. Changes: - CI matrix: [18, 20, 22] → [20, 22, 24] - package.json engines: >=16.7.0 → >=20.0.0 - Removed Node 18 conditional in CI (c8 coverage works on all 20+) - Simplified CI to single test:coverage step for all versions 797/797 tests pass on Node 24. |
||
|
|
ffc1a2efa0 |
fix(ci): use bash shell on Windows for glob expansion in test steps
Windows PowerShell doesn't expand `tests/*.test.cjs` globs, causing the test runner to fail with "Could not find" on Windows Node 20. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
f9fc2a3f33 |
fix(ci): pin action SHAs and enforce coverage on all events
- Pin actions/checkout and actions/setup-node to SHA for supply chain safety - Run coverage threshold on all events (not just PRs) so direct pushes to main are also gated - Remove .planning/ artifact that was dev bookkeeping Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
896499120b |
fix(ci): add Node 18 skip condition for c8 v11 coverage step
c8 v11 requires Node 20+ (engines: ^20.0.0 || >=22.0.0). Node 18 now runs plain npm test on PRs to avoid engine mismatch. Also removes redundant --exclude flag from test:coverage script. Also: fix ROADMAP.md progress table alignment (rows 7-12), mark Phase 7 complete, add requirements-completed to 09-01-SUMMARY.md. |
||
|
|
21c898b2cc | ci(12-02): run coverage check on PRs with 70% line threshold | ||
|
|
9aa9695731 |
feat(06-01): create GitHub Actions test workflow
- Tests workflow: push/PR to main + workflow_dispatch - 3x3 matrix: ubuntu/macos/windows x Node 18/20/22 (9 jobs) - Concurrency groups with cancel-in-progress - 10-minute timeout, fail-fast enabled - Steps: checkout, setup-node with npm cache, npm ci, npm test |
||
|
|
b85247a3b8 | Add workflow to auto-label new issues with needs-triage | ||
|
|
339e911299 |
chore: remove GitHub Actions release workflow
npm publish is the only distribution channel needed. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |