From f4ec92341cb20d028be12035cbd5a0d23b82fd49 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Thu, 23 Apr 2026 11:08:12 -0700 Subject: [PATCH] feat(skills): queue-aware /ship + drift abort in /land-and-deploy + advisory in /review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ship Step 12: queue-aware version pick (FRESH path) + drift detection (ALREADY_BUMPED path). Prompts user to rebump when queue moved, runs the full ship metadata path (VERSION, package.json, CHANGELOG header, PR title) on the rebump so nothing goes stale. ship Step 19: PR title format v : — version ALWAYS first. Rerun path updates title (not just body) when VERSION changed. land-and-deploy Step 3.4: detect drift, ABORT with instruction to rerun /ship. Never auto-mutates from land. review Step 3.4: advisory one-line queue status. Non-blocking. Goldens refreshed for all three hosts (claude/codex/factory). Co-Authored-By: Claude Opus 4.7 (1M context) --- land-and-deploy/SKILL.md | 43 +++++++++++++++++++++ land-and-deploy/SKILL.md.tmpl | 43 +++++++++++++++++++++ review/SKILL.md | 22 +++++++++++ review/SKILL.md.tmpl | 22 +++++++++++ ship/SKILL.md | 44 ++++++++++++++++++---- ship/SKILL.md.tmpl | 44 ++++++++++++++++++---- test/fixtures/golden/claude-ship-SKILL.md | 44 ++++++++++++++++++---- test/fixtures/golden/codex-ship-SKILL.md | 44 ++++++++++++++++++---- test/fixtures/golden/factory-ship-SKILL.md | 44 ++++++++++++++++++---- 9 files changed, 310 insertions(+), 40 deletions(-) diff --git a/land-and-deploy/SKILL.md b/land-and-deploy/SKILL.md index 921c4d5d..101d507f 100644 --- a/land-and-deploy/SKILL.md +++ b/land-and-deploy/SKILL.md @@ -1233,6 +1233,49 @@ If timeout (15 min): **STOP.** "CI has been running for over 15 minutes — that --- +## Step 3.4: VERSION drift detection (workspace-aware ship) + +Before gathering readiness evidence, verify that the VERSION this PR claims is still the next free slot. A sibling workspace may have shipped and landed since `/ship` ran, leaving this PR's VERSION stale. + +```bash +BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "") +BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main) +BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "") + +# Imply bump level by comparing branch VERSION to base (crude but good enough for drift detection) +# We don't need the exact original level — we just need "a level" that passes to the util. +# If the minor digit advanced, call it minor; patch digit, patch; etc. If base > branch, skip (not ours to land). +# For simplicity: use "patch" as a conservative default; util handles collision-past regardless of input level. +QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base "$BASE_BRANCH" \ + --bump patch \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') +NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty') +OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') +``` + +Behavior: + +1. If `OFFLINE=true` or the util fails: print `⚠ VERSION drift check unavailable (util offline) — proceeding with PR version v`. Continue to Step 3.5. CI's version-gate job is the backstop. + +2. If `BRANCH_VERSION` is already `>=` than `NEXT_SLOT`: no drift (or our PR is ahead of the queue). Continue. + +3. If drift is detected (a PR landed ahead of us and `BRANCH_VERSION < NEXT_SLOT`): **STOP** and print exactly: + ``` + ⚠ VERSION drift detected. + This PR claims: v + Next free slot: v (queue moved since last /ship) + + Rerun /ship from the feature branch to reconcile. /ship's ALREADY_BUMPED + branch will detect the drift and rewrite VERSION + CHANGELOG header + PR title + atomically. Do NOT merge from here — the landed PR would overwrite the other + branch's CHANGELOG entry or land with a duplicate version header. + ``` + + Exit non-zero. Do NOT auto-bump from `/land-and-deploy` — rerunning `/ship` is the clean path (it already handles VERSION + package.json + CHANGELOG header + PR title atomically via Step 12 ALREADY_BUMPED detection). + +--- + ## Step 3.5: Pre-merge readiness gate **This is the critical safety check before an irreversible merge.** The merge cannot diff --git a/land-and-deploy/SKILL.md.tmpl b/land-and-deploy/SKILL.md.tmpl index c5a35110..a08debea 100644 --- a/land-and-deploy/SKILL.md.tmpl +++ b/land-and-deploy/SKILL.md.tmpl @@ -328,6 +328,49 @@ If timeout (15 min): **STOP.** "CI has been running for over 15 minutes — that --- +## Step 3.4: VERSION drift detection (workspace-aware ship) + +Before gathering readiness evidence, verify that the VERSION this PR claims is still the next free slot. A sibling workspace may have shipped and landed since `/ship` ran, leaving this PR's VERSION stale. + +```bash +BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "") +BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main) +BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "") + +# Imply bump level by comparing branch VERSION to base (crude but good enough for drift detection) +# We don't need the exact original level — we just need "a level" that passes to the util. +# If the minor digit advanced, call it minor; patch digit, patch; etc. If base > branch, skip (not ours to land). +# For simplicity: use "patch" as a conservative default; util handles collision-past regardless of input level. +QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base "$BASE_BRANCH" \ + --bump patch \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') +NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty') +OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') +``` + +Behavior: + +1. If `OFFLINE=true` or the util fails: print `⚠ VERSION drift check unavailable (util offline) — proceeding with PR version v`. Continue to Step 3.5. CI's version-gate job is the backstop. + +2. If `BRANCH_VERSION` is already `>=` than `NEXT_SLOT`: no drift (or our PR is ahead of the queue). Continue. + +3. If drift is detected (a PR landed ahead of us and `BRANCH_VERSION < NEXT_SLOT`): **STOP** and print exactly: + ``` + ⚠ VERSION drift detected. + This PR claims: v + Next free slot: v (queue moved since last /ship) + + Rerun /ship from the feature branch to reconcile. /ship's ALREADY_BUMPED + branch will detect the drift and rewrite VERSION + CHANGELOG header + PR title + atomically. Do NOT merge from here — the landed PR would overwrite the other + branch's CHANGELOG entry or land with a duplicate version header. + ``` + + Exit non-zero. Do NOT auto-bump from `/land-and-deploy` — rerunning `/ship` is the clean path (it already handles VERSION + package.json + CHANGELOG header + PR title atomically via Step 12 ALREADY_BUMPED detection). + +--- + ## Step 3.5: Pre-merge readiness gate **This is the critical safety check before an irreversible merge.** The merge cannot diff --git a/review/SKILL.md b/review/SKILL.md index 6b82d502..ea0aa506 100644 --- a/review/SKILL.md +++ b/review/SKILL.md @@ -1117,6 +1117,28 @@ git fetch origin --quiet Run `git diff origin/` to get the full diff. This includes both committed and uncommitted changes against the latest base branch. +## Step 3.4: Workspace-aware queue status (advisory) + +Check whether this PR's claimed VERSION still points at a free slot in the queue. Advisory only — never blocks review; just informs the reviewer about landing-order risk. + +```bash +BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "") +BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main) +BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "") +QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base "$BASE_BRANCH" \ + --bump patch \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') +NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty') +CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length // 0') +OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') +``` + +- If `OFFLINE=true`: skip this section (no signal to report). +- Otherwise, include ONE line in the review output: `Version claimed: v. Queue: PR(s) ahead. ` where VERDICT is either `Slot free` (if `BRANCH_VERSION >= NEXT_SLOT`) or `⚠ queue moved — rerun /ship to reconcile v → v`. + +--- + ## Step 3.5: Slop scan (advisory) Run a slop scan on changed files to catch AI code quality issues (empty catches, diff --git a/review/SKILL.md.tmpl b/review/SKILL.md.tmpl index 7863639d..fada6911 100644 --- a/review/SKILL.md.tmpl +++ b/review/SKILL.md.tmpl @@ -74,6 +74,28 @@ git fetch origin --quiet Run `git diff origin/` to get the full diff. This includes both committed and uncommitted changes against the latest base branch. +## Step 3.4: Workspace-aware queue status (advisory) + +Check whether this PR's claimed VERSION still points at a free slot in the queue. Advisory only — never blocks review; just informs the reviewer about landing-order risk. + +```bash +BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "") +BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main) +BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "") +QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base "$BASE_BRANCH" \ + --bump patch \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') +NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty') +CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length // 0') +OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') +``` + +- If `OFFLINE=true`: skip this section (no signal to report). +- Otherwise, include ONE line in the review output: `Version claimed: v. Queue: PR(s) ahead. ` where VERDICT is either `Slot free` (if `BRANCH_VERSION >= NEXT_SLOT`) or `⚠ queue moved — rerun /ship to reconcile v → v`. + +--- + ## Step 3.5: Slop scan (advisory) Run a slop scan on changed files to catch AI code quality issues (empty catches, diff --git a/ship/SKILL.md b/ship/SKILL.md index e56262ed..1a8d951a 100644 --- a/ship/SKILL.md +++ b/ship/SKILL.md @@ -2522,8 +2522,8 @@ fi Read the `STATE:` line and dispatch: - **FRESH** → proceed with the bump action below (steps 1–4). -- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step. -- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. +- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v but next available is v (queue moved). A) Rebump to v and rewrite CHANGELOG header + PR title (recommended), B) Keep v — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`. +- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.) - **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative. 1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`) @@ -2536,9 +2536,33 @@ Read the `STATE:` line and dispatch: - **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added - **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes -3. Compute the new version: - - Bumping a digit resets all digits to its right to 0 - - Example: `0.19.1.0` + PATCH → `0.19.2.0` + Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot. + +3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user: + + ```bash + QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base \ + --bump "$BUMP_LEVEL" \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') + NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty') + CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length') + ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length') + OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') + REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""') + ``` + + - If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue. + - If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance: + ``` + Queue on (vBASE_VERSION): + # → v [⚠ collision with #] + Active sibling workspaces (WIP, not yet PR'd): + → v (committed Nh ago) + Your branch will claim: vNEW_VERSION () + ``` + - If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace has v committed h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first." + - Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump. 4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`. @@ -2879,7 +2903,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20. +If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. + +**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. + +Print the existing URL and continue to Step 20. If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0. @@ -2947,7 +2975,7 @@ you missed it.> **If GitHub:** ```bash -gh pr create --base --title ": " --body "$(cat <<'EOF' +gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF )" @@ -2956,7 +2984,7 @@ EOF **If GitLab:** ```bash -glab mr create -b -t ": " -d "$(cat <<'EOF' +glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF )" diff --git a/ship/SKILL.md.tmpl b/ship/SKILL.md.tmpl index 9eab6d33..b6a19bcb 100644 --- a/ship/SKILL.md.tmpl +++ b/ship/SKILL.md.tmpl @@ -451,8 +451,8 @@ fi Read the `STATE:` line and dispatch: - **FRESH** → proceed with the bump action below (steps 1–4). -- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step. -- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. +- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v but next available is v (queue moved). A) Rebump to v and rewrite CHANGELOG header + PR title (recommended), B) Keep v — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`. +- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.) - **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative. 1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`) @@ -465,9 +465,33 @@ Read the `STATE:` line and dispatch: - **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added - **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes -3. Compute the new version: - - Bumping a digit resets all digits to its right to 0 - - Example: `0.19.1.0` + PATCH → `0.19.2.0` + Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot. + +3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user: + + ```bash + QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base \ + --bump "$BUMP_LEVEL" \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') + NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty') + CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length') + ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length') + OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') + REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""') + ``` + + - If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue. + - If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance: + ``` + Queue on (vBASE_VERSION): + # → v [⚠ collision with #] + Active sibling workspaces (WIP, not yet PR'd): + → v (committed Nh ago) + Your branch will claim: vNEW_VERSION () + ``` + - If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace has v committed h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first." + - Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump. 4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`. @@ -768,7 +792,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20. +If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. + +**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. + +Print the existing URL and continue to Step 20. If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0. @@ -836,7 +864,7 @@ you missed it.> **If GitHub:** ```bash -gh pr create --base --title ": " --body "$(cat <<'EOF' +gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF )" @@ -845,7 +873,7 @@ EOF **If GitLab:** ```bash -glab mr create -b -t ": " -d "$(cat <<'EOF' +glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF )" diff --git a/test/fixtures/golden/claude-ship-SKILL.md b/test/fixtures/golden/claude-ship-SKILL.md index e56262ed..1a8d951a 100644 --- a/test/fixtures/golden/claude-ship-SKILL.md +++ b/test/fixtures/golden/claude-ship-SKILL.md @@ -2522,8 +2522,8 @@ fi Read the `STATE:` line and dispatch: - **FRESH** → proceed with the bump action below (steps 1–4). -- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step. -- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. +- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v but next available is v (queue moved). A) Rebump to v and rewrite CHANGELOG header + PR title (recommended), B) Keep v — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`. +- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.) - **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative. 1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`) @@ -2536,9 +2536,33 @@ Read the `STATE:` line and dispatch: - **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added - **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes -3. Compute the new version: - - Bumping a digit resets all digits to its right to 0 - - Example: `0.19.1.0` + PATCH → `0.19.2.0` + Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot. + +3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user: + + ```bash + QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base \ + --bump "$BUMP_LEVEL" \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') + NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty') + CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length') + ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length') + OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') + REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""') + ``` + + - If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue. + - If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance: + ``` + Queue on (vBASE_VERSION): + # → v [⚠ collision with #] + Active sibling workspaces (WIP, not yet PR'd): + → v (committed Nh ago) + Your branch will claim: vNEW_VERSION () + ``` + - If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace has v committed h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first." + - Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump. 4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`. @@ -2879,7 +2903,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20. +If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. + +**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. + +Print the existing URL and continue to Step 20. If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0. @@ -2947,7 +2975,7 @@ you missed it.> **If GitHub:** ```bash -gh pr create --base --title ": " --body "$(cat <<'EOF' +gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF )" @@ -2956,7 +2984,7 @@ EOF **If GitLab:** ```bash -glab mr create -b -t ": " -d "$(cat <<'EOF' +glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF )" diff --git a/test/fixtures/golden/codex-ship-SKILL.md b/test/fixtures/golden/codex-ship-SKILL.md index a01e0887..21b3c9d4 100644 --- a/test/fixtures/golden/codex-ship-SKILL.md +++ b/test/fixtures/golden/codex-ship-SKILL.md @@ -2137,8 +2137,8 @@ fi Read the `STATE:` line and dispatch: - **FRESH** → proceed with the bump action below (steps 1–4). -- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step. -- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. +- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v but next available is v (queue moved). A) Rebump to v and rewrite CHANGELOG header + PR title (recommended), B) Keep v — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`. +- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.) - **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative. 1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`) @@ -2151,9 +2151,33 @@ Read the `STATE:` line and dispatch: - **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added - **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes -3. Compute the new version: - - Bumping a digit resets all digits to its right to 0 - - Example: `0.19.1.0` + PATCH → `0.19.2.0` + Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot. + +3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user: + + ```bash + QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base \ + --bump "$BUMP_LEVEL" \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') + NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty') + CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length') + ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length') + OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') + REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""') + ``` + + - If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue. + - If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance: + ``` + Queue on (vBASE_VERSION): + # → v [⚠ collision with #] + Active sibling workspaces (WIP, not yet PR'd): + → v (committed Nh ago) + Your branch will claim: vNEW_VERSION () + ``` + - If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace has v committed h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first." + - Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump. 4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`. @@ -2494,7 +2518,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20. +If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. + +**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. + +Print the existing URL and continue to Step 20. If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0. @@ -2562,7 +2590,7 @@ you missed it.> **If GitHub:** ```bash -gh pr create --base --title ": " --body "$(cat <<'EOF' +gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF )" @@ -2571,7 +2599,7 @@ EOF **If GitLab:** ```bash -glab mr create -b -t ": " -d "$(cat <<'EOF' +glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF )" diff --git a/test/fixtures/golden/factory-ship-SKILL.md b/test/fixtures/golden/factory-ship-SKILL.md index 9aa7a596..a0685b49 100644 --- a/test/fixtures/golden/factory-ship-SKILL.md +++ b/test/fixtures/golden/factory-ship-SKILL.md @@ -2513,8 +2513,8 @@ fi Read the `STATE:` line and dispatch: - **FRESH** → proceed with the bump action below (steps 1–4). -- **ALREADY_BUMPED** → skip the bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. Continue to the next step. -- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. +- **ALREADY_BUMPED** → skip the bump by default, BUT check for queue drift first: call `bin/gstack-next-version` with the implied bump level (derived from `CURRENT_VERSION` vs `BASE_VERSION`), compare its `.version` against `CURRENT_VERSION`. If they differ (queue moved since last ship), use **AskUserQuestion**: "VERSION drift detected: you claim v but next available is v (queue moved). A) Rebump to v and rewrite CHANGELOG header + PR title (recommended), B) Keep v — will be rejected by CI version-gate until resolved." If A, treat this as FRESH with `NEW_VERSION=` and run steps 1-4 (which will also trigger Step 13 CHANGELOG header rewrite and Step 19 PR title rewrite). If B, reuse `CURRENT_VERSION` and warn that CI will likely reject. If util is offline, warn and reuse `CURRENT_VERSION`. +- **DRIFT_STALE_PKG** → a prior `/ship` bumped `VERSION` but failed to update `package.json`. Run the sync-only repair block below (after step 4). Do NOT re-bump. Reuse `CURRENT_VERSION` for CHANGELOG and PR body. (Queue check still runs in ALREADY_BUMPED terms after repair.) - **DRIFT_UNEXPECTED** → `/ship` has halted (exit 1). Resolve manually; /ship cannot tell which file is authoritative. 1. Read the current `VERSION` file (4-digit format: `MAJOR.MINOR.PATCH.MICRO`) @@ -2527,9 +2527,33 @@ Read the `STATE:` line and dispatch: - **MINOR** (2nd digit): **ASK the user** if ANY feature signal is detected, OR 500+ lines changed, OR new modules/packages added - **MAJOR** (1st digit): **ASK the user** — only for milestones or breaking changes -3. Compute the new version: - - Bumping a digit resets all digits to its right to 0 - - Example: `0.19.1.0` + PATCH → `0.19.2.0` + Save the chosen level as `BUMP_LEVEL` (one of `major`, `minor`, `patch`, `micro`). This is the user-intended level. The next step decides *placement* — the level stays the same even if queue-aware allocation has to advance past a claimed slot. + +3. **Queue-aware version pick (workspace-aware ship, v1.6.4.0+).** Call `bin/gstack-next-version` to see what's already claimed by open PRs + active sibling Conductor worktrees, then render the queue state to the user: + + ```bash + QUEUE_JSON=$(bun run bin/gstack-next-version \ + --base \ + --bump "$BUMP_LEVEL" \ + --current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}') + NEW_VERSION=$(echo "$QUEUE_JSON" | jq -r '.version // empty') + CLAIMED_COUNT=$(echo "$QUEUE_JSON" | jq -r '.claimed | length') + ACTIVE_SIBLING_COUNT=$(echo "$QUEUE_JSON" | jq -r '.active_siblings | length') + OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false') + REASON=$(echo "$QUEUE_JSON" | jq -r '.reason // ""') + ``` + + - If `OFFLINE=true` or the util fails (auth expired, no `gh`/`glab`, network): fall back to local `BUMP_LEVEL` arithmetic (bump `BASE_VERSION` at the chosen level). Print `⚠ workspace-aware ship offline — using local bump only`. Continue. + - If `CLAIMED_COUNT > 0`: render the queue table to the user so they can see landing order at a glance: + ``` + Queue on (vBASE_VERSION): + # → v [⚠ collision with #] + Active sibling workspaces (WIP, not yet PR'd): + → v (committed Nh ago) + Your branch will claim: vNEW_VERSION () + ``` + - If `ACTIVE_SIBLING_COUNT > 0` and any active sibling's VERSION is `>= NEW_VERSION`, use **AskUserQuestion**: "Sibling workspace has v committed h ago but hasn't PR'd yet. Wait for them to ship first, or advance past? A) Advance past (recommended for unrelated work), B) Abort /ship and sync up with sibling first." + - Validate `NEW_VERSION` matches `MAJOR.MINOR.PATCH.MICRO`. If util returns an empty or malformed version, fall back to local bump. 4. **Validate** `NEW_VERSION` and write it to **both** `VERSION` and `package.json`. This block runs only when `STATE: FRESH`. @@ -2870,7 +2894,11 @@ gh pr view --json url,number,state -q 'if .state == "OPEN" then "PR #\(.number): glab mr view -F json 2>/dev/null | jq -r 'if .state == "opened" then "MR_EXISTS" else "NO_MR" end' 2>/dev/null || echo "NO_MR" ``` -If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. Print the existing URL and continue to Step 20. +If an **open** PR/MR already exists: **update** the PR body using `gh pr edit --body "..."` (GitHub) or `glab mr update -d "..."` (GitLab). Always regenerate the PR body from scratch using this run's fresh results (test output, coverage audit, review findings, adversarial review, TODOS summary, documentation_section from Step 18). Never reuse stale PR body content from a prior run. + +**Also update the PR title** if the version changed on rerun. PR titles use the workspace-aware format `v : ` — version ALWAYS first. If the current title's version prefix doesn't match `NEW_VERSION`, run `gh pr edit --title "v$NEW_VERSION : "` (or the `glab mr update -t ...` equivalent). This keeps the title truthful when Step 12's queue-drift detection rebumps a stale version. If the title has no `v` prefix (a custom title kept intentionally), leave the title alone — only rewrite titles that already follow the format. + +Print the existing URL and continue to Step 20. If no PR/MR exists: create a pull request (GitHub) or merge request (GitLab) using the platform detected in Step 0. @@ -2938,7 +2966,7 @@ you missed it.> **If GitHub:** ```bash -gh pr create --base --title ": " --body "$(cat <<'EOF' +gh pr create --base --title "v$NEW_VERSION : " --body "$(cat <<'EOF' EOF )" @@ -2947,7 +2975,7 @@ EOF **If GitLab:** ```bash -glab mr create -b -t ": " -d "$(cat <<'EOF' +glab mr create -b -t "v$NEW_VERSION : " -d "$(cat <<'EOF' EOF )"