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 )"