codebyplan 1.13.34 → 1.13.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.13.34";
17
+ VERSION = "1.13.36";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -1606,22 +1606,16 @@ function mergeEnabledPluginsIntoSettings(settings, plugins) {
1606
1606
  }
1607
1607
  return settings;
1608
1608
  }
1609
- function stripOwnedHooksFromSettings(settings) {
1609
+ function collapseEmptyHookBlocks(settings) {
1610
1610
  if (!settings.hooks) {
1611
- return settings;
1611
+ return;
1612
1612
  }
1613
1613
  for (const event of Object.keys(settings.hooks)) {
1614
1614
  const eventBlocks = settings.hooks[event];
1615
1615
  if (!eventBlocks) {
1616
1616
  continue;
1617
1617
  }
1618
- const survivingBlocks = [];
1619
- for (const block of eventBlocks) {
1620
- block.hooks = block.hooks.filter((e) => e._owner !== OWNER);
1621
- if (block.hooks.length > 0) {
1622
- survivingBlocks.push(block);
1623
- }
1624
- }
1618
+ const survivingBlocks = eventBlocks.filter((b) => b.hooks.length > 0);
1625
1619
  if (survivingBlocks.length > 0) {
1626
1620
  settings.hooks[event] = survivingBlocks;
1627
1621
  } else {
@@ -1631,6 +1625,54 @@ function stripOwnedHooksFromSettings(settings) {
1631
1625
  if (Object.keys(settings.hooks).length === 0) {
1632
1626
  delete settings.hooks;
1633
1627
  }
1628
+ }
1629
+ function stripOwnedHooksFromSettings(settings) {
1630
+ if (!settings.hooks) {
1631
+ return settings;
1632
+ }
1633
+ for (const event of Object.keys(settings.hooks)) {
1634
+ const eventBlocks = settings.hooks[event];
1635
+ if (!eventBlocks) {
1636
+ continue;
1637
+ }
1638
+ for (const block of eventBlocks) {
1639
+ block.hooks = block.hooks.filter((e) => e._owner !== OWNER);
1640
+ }
1641
+ }
1642
+ collapseEmptyHookBlocks(settings);
1643
+ return settings;
1644
+ }
1645
+ function pruneMissingManagedHooks(settings, hooksJson, onPruned) {
1646
+ if (!settings.hooks) {
1647
+ return settings;
1648
+ }
1649
+ const liveIdsByEvent = /* @__PURE__ */ new Map();
1650
+ for (const [event, matchers] of Object.entries(hooksJson.hooks)) {
1651
+ const ids = /* @__PURE__ */ new Set();
1652
+ for (const block of matchers) {
1653
+ for (const cmd of block.hooks) {
1654
+ ids.add(extractHookId(cmd.command));
1655
+ }
1656
+ }
1657
+ liveIdsByEvent.set(event, ids);
1658
+ }
1659
+ for (const event of Object.keys(settings.hooks)) {
1660
+ const eventBlocks = settings.hooks[event];
1661
+ if (!eventBlocks) {
1662
+ continue;
1663
+ }
1664
+ const liveIds = liveIdsByEvent.get(event) ?? /* @__PURE__ */ new Set();
1665
+ for (const block of eventBlocks) {
1666
+ block.hooks = block.hooks.filter((e) => {
1667
+ const shouldPrune = e._owner === OWNER && e._hook_id !== void 0 && !liveIds.has(e._hook_id);
1668
+ if (shouldPrune && onPruned) {
1669
+ onPruned(e._hook_id);
1670
+ }
1671
+ return !shouldPrune;
1672
+ });
1673
+ }
1674
+ }
1675
+ collapseEmptyHookBlocks(settings);
1634
1676
  return settings;
1635
1677
  }
1636
1678
  var OWNER, PLACEHOLDER_RE, REPLACEMENT, SCALAR_BASE_KEYS, DEPRECATED_BASE_KEYS;
@@ -2558,9 +2600,9 @@ async function runLspFull(projectPath, opts = {}) {
2558
2600
  ` Detected LSP servers: ${servers.map((s) => s.plugin).join(", ")}
2559
2601
  `
2560
2602
  );
2561
- const settingsPath = join10(projectPath, ".claude", "settings.json");
2603
+ const settingsLocalPath = join10(projectPath, ".claude", "settings.local.json");
2562
2604
  let settings = {};
2563
- const existingSettingsRaw = await readJsonFile(settingsPath);
2605
+ const existingSettingsRaw = await readJsonFile(settingsLocalPath);
2564
2606
  if (existingSettingsRaw) {
2565
2607
  settings = existingSettingsRaw;
2566
2608
  }
@@ -2569,15 +2611,15 @@ async function runLspFull(projectPath, opts = {}) {
2569
2611
  servers.map((s) => s.plugin)
2570
2612
  );
2571
2613
  if (dryRun) {
2572
- console.log(` [dry-run] would update ${settingsPath}`);
2614
+ console.log(` [dry-run] would update ${settingsLocalPath}`);
2573
2615
  } else {
2574
2616
  await mkdir4(join10(projectPath, ".claude"), { recursive: true });
2575
2617
  await writeFile6(
2576
- settingsPath,
2618
+ settingsLocalPath,
2577
2619
  JSON.stringify(settings, null, 2) + "\n",
2578
2620
  "utf-8"
2579
2621
  );
2580
- console.log(` Updated ${settingsPath}`);
2622
+ console.log(` Updated ${settingsLocalPath}`);
2581
2623
  }
2582
2624
  const lspJsonPath = join10(projectPath, ".codebyplan", "lsp.json");
2583
2625
  const lspJsonContent = {
@@ -2648,7 +2690,7 @@ async function runLspFull(projectPath, opts = {}) {
2648
2690
  ` Enabled plugins: ${pluginNames.length > 0 ? pluginNames.join(", ") : "none"}`
2649
2691
  );
2650
2692
  console.log(
2651
- ` (settings.json key format: <plugin>@claude-plugins-official)`
2693
+ ` (settings.local.json key format: <plugin>@claude-plugins-official)`
2652
2694
  );
2653
2695
  if (autoInstalled.length > 0) {
2654
2696
  console.log(
@@ -2811,6 +2853,13 @@ async function runInstall(opts, deps = {}) {
2811
2853
  fs3.readFileSync(hooksJsonPath, "utf8")
2812
2854
  );
2813
2855
  mergeHooksIntoSettings(existingSettings, hooksJson);
2856
+ pruneMissingManagedHooks(
2857
+ existingSettings,
2858
+ hooksJson,
2859
+ opts.verbose ? (id) => console.log(
2860
+ `${opts.dryRun ? "[dry-run] would prune" : "pruned"} removed managed hook: ${id}`
2861
+ ) : void 0
2862
+ );
2814
2863
  }
2815
2864
  if (!opts.dryRun) {
2816
2865
  fs3.mkdirSync(path4.dirname(settingsPath), { recursive: true });
@@ -5628,6 +5677,13 @@ function defaultPrBody(feat, base) {
5628
5677
  Merge \`${feat}\` \u2192 \`${base}\`.
5629
5678
  `;
5630
5679
  }
5680
+ function evaluateChecks(checks) {
5681
+ const failed = checks.filter(
5682
+ (c) => c.bucket !== "pass" && c.bucket !== "skipping" && c.bucket !== "pending"
5683
+ ).map((c) => c.name);
5684
+ const pending = checks.filter((c) => c.bucket === "pending").length;
5685
+ return { passed: failed.length === 0 && pending === 0, failed, pending };
5686
+ }
5631
5687
  async function pollChecks(feat, timeoutSeconds, cwd, _sleepMs = (ms) => new Promise((r) => setTimeout(r, ms))) {
5632
5688
  const deadline = Date.now() + timeoutSeconds * 1e3;
5633
5689
  const POLL_INTERVAL_MS = 15e3;
@@ -5640,7 +5696,7 @@ async function pollChecks(feat, timeoutSeconds, cwd, _sleepMs = (ms) => new Prom
5640
5696
  while (Date.now() < deadline) {
5641
5697
  const ghResult = spawnSync6(
5642
5698
  "gh",
5643
- ["pr", "checks", feat, "--json", "name,state,conclusion"],
5699
+ ["pr", "checks", feat, "--json", "name,state,bucket"],
5644
5700
  {
5645
5701
  cwd,
5646
5702
  encoding: "utf-8",
@@ -5678,17 +5734,14 @@ async function pollChecks(feat, timeoutSeconds, cwd, _sleepMs = (ms) => new Prom
5678
5734
  return { passed: true };
5679
5735
  }
5680
5736
  iteration++;
5681
- const pending = checks.filter(
5682
- (c) => c.state === "PENDING" || c.state === "QUEUED" || c.state === "IN_PROGRESS"
5683
- );
5684
- const failed = checks.filter(
5685
- (c) => c.state === "COMPLETED" && c.conclusion !== null && c.conclusion !== "SUCCESS" && c.conclusion !== "NEUTRAL" && c.conclusion !== "SKIPPED"
5686
- );
5687
- if (failed.length > 0) {
5688
- const names = failed.map((c) => c.name).join(", ");
5689
- return { passed: false, reason: `Checks failed: ${names}` };
5737
+ const result = evaluateChecks(checks);
5738
+ if (result.failed.length > 0) {
5739
+ return {
5740
+ passed: false,
5741
+ reason: `Checks failed: ${result.failed.join(", ")}`
5742
+ };
5690
5743
  }
5691
- if (pending.length === 0) {
5744
+ if (result.pending === 0) {
5692
5745
  return { passed: true };
5693
5746
  }
5694
5747
  await _sleepMs(POLL_INTERVAL_MS);
@@ -8806,6 +8859,13 @@ async function runUpdate(opts, deps = {}) {
8806
8859
  fs7.readFileSync(hooksJsonPath, "utf8")
8807
8860
  );
8808
8861
  mergeHooksIntoSettings(existingSettings, hooksJson);
8862
+ pruneMissingManagedHooks(
8863
+ existingSettings,
8864
+ hooksJson,
8865
+ opts.verbose ? (id) => console.log(
8866
+ `${opts.dryRun ? "[dry-run] would prune" : "pruned"} removed managed hook: ${id}`
8867
+ ) : void 0
8868
+ );
8809
8869
  }
8810
8870
  if (!opts.dryRun) {
8811
8871
  fs7.mkdirSync(path8.dirname(settingsPath), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.34",
3
+ "version": "1.13.36",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@ paths:
10
10
  - ".claude/skills/cbp-checkpoint-end/**"
11
11
  - ".claude/skills/cbp-git-worktree-remove/**"
12
12
  - ".claude/skills/cbp-ship-main/**"
13
+ - ".claude/skills/cbp-standalone-task-complete/**"
13
14
  ---
14
15
 
15
16
  # Supabase Branch Lifecycle
@@ -50,6 +51,7 @@ The Supabase branch is removed wherever the git branch is deleted:
50
51
  | `cbp-checkpoint-end` | stale-branch cleanup + current feat-branch delete on ship |
51
52
  | `cbp-git-worktree-remove` | worktree teardown removes the coupled Supabase branch |
52
53
  | `cbp-ship-main` | `branch_deleted` event after PR merge |
54
+ | `cbp-standalone-task-complete` | `branch_deleted` event after standalone PR merge (Step 7.3) |
53
55
 
54
56
  Deletion is **existence-checked and idempotent** — a not-found response is treated as
55
57
  success. This tolerates the GitHub integration auto-deleting the preview branch on PR
@@ -93,7 +95,7 @@ or auto-created by the GitHub integration — both paths use the same branch nam
93
95
  | Role | Skill |
94
96
  |---|---|
95
97
  | Create (lazy) | `cbp-supabase-migrate` (Step 2.3) |
96
- | Delete | `cbp-checkpoint-end`, `cbp-git-worktree-remove`, `cbp-ship-main` |
98
+ | Delete | `cbp-checkpoint-end`, `cbp-standalone-task-complete`, `cbp-git-worktree-remove`, `cbp-ship-main` |
97
99
  | PR gate | `cbp-supabase-branch-check` |
98
100
 
99
101
  Each skill in the Skill Map above carries an inline back-reference to this rule at its create or teardown step.
@@ -38,6 +38,24 @@ Write to `/tmp/cbp-ship-main-body.md`:
38
38
  - /cbp-checkpoint-check passed
39
39
  ```
40
40
 
41
+ ### Step 2.5: Supabase Branch Pre-Merge Gate
42
+
43
+ **PR-existence pre-check** — `codebyplan ship` (Step 3) is what pushes the branch and creates the PR, so on the FIRST ship of a branch no PR exists yet and a `pre_merge` gate would self-defeatingly block the very step that creates it. Probe first:
44
+
45
+ ```bash
46
+ PR_EXISTS=$(gh pr list --head "$(git branch --show-current)" --json number --jq 'length' 2>/dev/null || echo 0)
47
+ ```
48
+
49
+ - `PR_EXISTS` is `0` → skip this gate and proceed directly to Step 3 with a one-line note: `Supabase pre-merge gate skipped (no PR yet) — the generic check-poll inside codebyplan ship gates Supabase Preview as a required check.`
50
+ - `PR_EXISTS` ≥ `1` → run the gate: invoke the `cbp-supabase-branch-check` skill with `--mode pre_merge --target production` and parse the fenced JSON block it emits (`status`, `reason`, optional `failed_step` / `log_excerpt`).
51
+
52
+ Gate dispositions:
53
+
54
+ - `status: "passed"` or `status: "skipped"` → proceed to Step 3.
55
+ - `status: "blocked"` or `status: "pending_pr"` → surface `reason` (plus `failed_step` and `log_excerpt` when present) and STOP — do NOT invoke `codebyplan ship`.
56
+
57
+ **Dual-gate note**: this dedicated gate adds DB-failure diagnostics (`failed_step`, `log_excerpt` pulled from Supabase logs) that the generic check-poll inside `codebyplan ship` cannot produce. The generic poll (`pollChecks` → `evaluateChecks` in `src/lib/ship.ts`, gh bucket model) still runs after PR creation and gates Supabase Preview as one required check among all checks — the two gates are complementary, not redundant.
58
+
41
59
  ### Step 3: Invoke `codebyplan ship`
42
60
 
43
61
  ```bash
@@ -53,7 +71,7 @@ Pass `--dry-run` through if the skill was invoked with a dry-run arg.
53
71
 
54
72
  Parse JSON from Step 3. Report `pr_url`, `merge_commit`, `branch_deleted`. If `checks_failed: true`, surface `checks_failure_reason` and stop.
55
73
 
56
- > **gh false-negative workaround.** `codebyplan ship` can report `checks_failed: true` when the underlying `gh` query reads a stale/mismatched check field (it queries `conclusion`/`state` and the GitHub API can lag). Before treating the stop as final, verify the real status: `gh pr checks <PR> --watch`. If every required check is green, merge manually with `gh pr merge <PR> --merge` — add `--admin` ONLY to escape a transient secondary-rate-limit loop, never to bypass a genuinely-failing gate. Never auto-merge silently on a `checks_failed` report; this verification is a manual decision.
74
+ > **gh false-negative workaround.** `codebyplan ship` can report `checks_failed: true` when the underlying `gh` query reads a stale/mismatched check field (the poller queries the gh `bucket` field — `pass|fail|pending|skipping|cancel` — and fail-safe-blocks anything outside `pass`/`skipping`, so an API lag or an unrecognized bucket reads as a failure). Before treating the stop as final, verify the real status: `gh pr checks <PR> --watch`. If every required check is green, merge manually with `gh pr merge <PR> --merge` — add `--admin` ONLY to escape a transient secondary-rate-limit loop, never to bypass a genuinely-failing gate. Never auto-merge silently on a `checks_failed` report; this verification is a manual decision. (A `checks_failed` at this stage most likely pertains to a non-Supabase-Preview check — Supabase Preview was already gated in Step 2.5 when a PR pre-existed. If `gh pr checks <PR> --watch` shows Supabase Preview itself failing, investigate via `cbp-supabase-branch-check`'s `failed_step`/`log_excerpt` diagnostics before treating it as a false negative.)
57
75
 
58
76
  If `bumps[]` is present with any non-skipped entry, surface a **Version bumps** line per package — `<name>: <currentVersion> → <nextVersion>` — so the user sees what this PR will publish on merge.
59
77
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  scope: repo-only:codebyplan
3
3
  name: cbp-standalone-task-complete
4
- description: Complete a standalone task — merge feat branch and mark done
4
+ description: Complete a standalone task — ship feat branch to production via PR and mark done
5
5
  argument-hint: [task] # e.g. `45` (standalone TASK-45)
6
6
  effort: xhigh
7
7
  ---
@@ -125,24 +125,60 @@ Skip only when nothing was committed AND `/cbp-merge-main` reported already-up-t
125
125
 
126
126
  `update_standalone_task(task_id, context: { ...existing, files_changed: aggregated_files })`.
127
127
 
128
- ### Step 7: Standalone Task Branch Merge
128
+ ### Step 7: Standalone Task Branch Ship
129
129
 
130
130
  Only when current branch matches `feat/standalone-TASK-*`:
131
131
 
132
- 1. Read `.codebyplan/git.json` `branch_config.production` (default: `main`). Store as `PRODUCTION`.
133
- 2. `git checkout {PRODUCTION}`
134
- 3. `git merge {feat-branch} --no-ff -m "Merge {feat-branch}: {task title}"`
135
- 4. `git push origin {PRODUCTION}`
136
- 5. Delete feat branch local: `git branch -d {feat-branch}`
137
- 6. Delete feat branch remote: `git push origin --delete {feat-branch}`
132
+ #### Step 7.1 Compose PR body
138
133
 
139
- If merge has conflicts, stop immediately and ask the user to resolve. Do not delete the feat branch until the merge succeeds and is pushed.
134
+ Write `/tmp/cbp-ship-body-standalone-{N}.md`:
140
135
 
141
- If current branch is not `feat/standalone-TASK-*`, skip this step.
136
+ ```markdown
137
+ ## Summary
138
+
139
+ {standalone_task.title}
140
+
141
+ Standalone TASK-{N}
142
+
143
+ ## Test Plan
144
+
145
+ - All rounds passed testing-qa
146
+ - /cbp-standalone-task-check passed
147
+ ```
148
+
149
+ #### Step 7.2 — Invoke `codebyplan ship`
150
+
151
+ ```bash
152
+ codebyplan ship --body-file /tmp/cbp-ship-body-standalone-{N}.md --json
153
+ ```
154
+
155
+ `codebyplan ship` patch-bumps every changed workspace package and commits `chore(release): bump versions` on the feat branch BEFORE creating the PR, so the bump rides this same feat→main PR. Pass `--no-bump` for infra/doc-only standalone work where a version bump is not appropriate.
156
+
157
+ `codebyplan ship` pushes the feat branch, gets-or-creates the PR, polls required checks (including Supabase Preview) via the gh bucket model (`pollChecks` → `evaluateChecks` — pass|fail|pending|skipping|cancel; only pass/skipping are non-blocking), merges with `gh pr merge --merge`, checks out the base branch, and deletes the feat branch.
158
+
159
+ **On `checks_failed: true`**: surface `checks_failure_reason` and STOP — do NOT proceed to Step 7.5 (`complete_standalone_task` is never called on a failed ship).
160
+
161
+ > **gh false-negative workaround.** `codebyplan ship` can report `checks_failed: true` when the underlying `gh` query reads a stale/mismatched check field. Before treating the stop as final, verify with `gh pr checks <PR> --watch`. If every required check is genuinely green, merge manually with `gh pr merge <PR> --merge`. Never auto-merge silently on a `checks_failed` report — this verification is a manual decision.
162
+
163
+ Parse the JSON output and store: `pr_url`, `merge_commit`, `branch_deleted`, `feat_branch`, `checks_failed`, `checks_failure_reason`, `bumps[]`.
164
+
165
+ #### Step 7.3 — Supabase preview-branch teardown
166
+
167
+ > Lifecycle contract: see [[supabase-branch-lifecycle]].
168
+
169
+ When `branch_deleted === true` in the ship JSON:
170
+
171
+ - Read `FEAT_BRANCH` from the `feat_branch` field in the ship JSON — NOT from `git branch --show-current`. By the time Step 7.3 runs, `codebyplan ship` has already checked out the base branch, so the live branch is the base, not the feat branch.
172
+ - Call `mcp__supabase__list_branches` with `project_id: rrvtrumtkhrsbhcyrwvf`.
173
+ - Scan the returned list for an entry whose `name` exactly equals `FEAT_BRANCH`.
174
+ - If found: call `mcp__supabase__delete_branch` with its `branch_id`. Report the outcome.
175
+ - If not found: no-op silently — the GitHub integration may have already removed the preview branch on PR close; not-found is success, NOT an error.
176
+ - If the `list_branches` call itself fails (network, auth, or non-success response): emit a non-blocking warning that the Supabase preview branch for `FEAT_BRANCH` may still exist and should be verified in the dashboard. Never treat an API failure as a not-found success.
177
+ - Never delete the parent project `rrvtrumtkhrsbhcyrwvf` itself or any persistent/production branch.
142
178
 
143
179
  ### Step 7.5: Complete Standalone Task
144
180
 
145
- Note: `complete_standalone_task` is called only after the branch merge succeeds — the DB completion record reflects work that has landed in production.
181
+ Note: `complete_standalone_task` is called only after `codebyplan ship` succeeds (no `checks_failed`) — the DB completion record reflects work that has landed in production.
146
182
 
147
183
  Resolve caller worktree: `CALLER_WT=$(npx codebyplan resolve-worktree 2>/dev/null)`.
148
184
 
@@ -168,7 +204,10 @@ Apply the `cleanup` skill inline to remove orphan references to deleted/modified
168
204
  **Rounds**: [N] completed
169
205
  **Files**: [N] changed
170
206
  **Commit**: [hash]
171
- **Branch merged**: [feat-branch] → {PRODUCTION}
207
+ **PR**: [pr_url]
208
+ **Merge commit**: [merge_commit]
209
+ **Branch deleted**: [branch_deleted]
210
+ **Version bumps**: [<name>: <current> → <next> per package, or "none"]
172
211
  **Warnings**: [any QA / file-approval warnings from Step 3, or "none"]
173
212
  ```
174
213
 
@@ -187,7 +226,7 @@ Do NOT use AskUserQuestion for routing. Do NOT use the Skill tool to auto-trigge
187
226
  ## Key Rules
188
227
 
189
228
  - **`caller_worktree_id` is REQUIRED** for `complete_standalone_task`
190
- - **Branch merge lives here** (not in checkpoint-end) — standalone tasks self-ship to production
229
+ - **Branch shipping lives here** (not in checkpoint-end) — standalone tasks self-ship to production via `codebyplan ship`
191
230
  - **Single-directive routing** — no menus, no auto-triggers via Skill tool
192
231
  - **No checkpoint** — never check `checkpoint_id`, always treat task as standalone
193
232
  - **Never skippable prerequisites** — check_verdict READY + task_testing_output all_passed required
@@ -196,6 +235,7 @@ Do NOT use AskUserQuestion for routing. Do NOT use the Skill tool to auto-trigge
196
235
 
197
236
  - **Triggered by**: `/cbp-standalone-task-testing` emits directive (user runs it manually after seeing directive)
198
237
  - **Chain**: `/cbp-standalone-task-check` → `/cbp-standalone-task-testing` → `/cbp-standalone-task-complete`
238
+ - **Delegates to**: `codebyplan ship` CLI (Step 7 — PR creation, check polling, merge, branch cleanup)
199
239
  - **Reads**: MCP `get_current_standalone_task`, `get_standalone_tasks`, `get_standalone_rounds`
200
240
  - **Writes**: MCP `update_standalone_task`, `complete_standalone_task`
201
241
  - **Uses skills (inline, no sub-agent)**: `cleanup` (if deletions), `migration` (if exports renamed)
@@ -57,7 +57,7 @@ Invalid --mode value: <value>. Valid values: pre_pr_create, pre_merge.
57
57
 
58
58
  Infer `TARGET` when absent:
59
59
  - `pre_pr_create` → `integration`
60
- - `pre_merge` → detect via `gh pr view --json baseRefName --jq '.baseRefName'`; fallback to `integration` if no PR is open.
60
+ - `pre_merge` → detect via `gh pr view --json baseRefName --jq '.baseRefName'`; fallback to `integration` if no PR is open. Production-ship callers (e.g. `cbp-ship-main` Step 2.5) should pass `--target production` explicitly rather than rely on auto-detect.
61
61
 
62
62
  ## Step 1 — Read DB Paths Config
63
63
 
@@ -339,5 +339,6 @@ Both emit `status: passed` so callers proceed; the `reason` differs for traceabi
339
339
 
340
340
  ## Integration
341
341
 
342
- **Callers**: `/cbp-ship-main` invokes this skill pre-merge (mode=`pre_merge`, target=`production`, no override).
342
+ **Callers**: `cbp-ship-main` Step 2.5 invokes this skill pre-merge (`--mode pre_merge --target production`, only when a PR already exists for the branch); the caller proceeds on `passed` (Supabase Preview check green — including the CI-level `skipping` bucket, which Step 8 maps to `passed`) and `skipped` (no DB-path changes detected — the gate did not apply), and stops on `blocked`/`pending_pr`. Note: `status: "skipped"` is NOT the CI `skipping` bucket — the two are distinct layers.
343
+ **Canonical bucket contract**: the gh-checks bucket vocabulary used in Step 6 — `pass|fail|pending|skipping|cancel` — is the single canonical contract shared with `evaluateChecks()` in `packages/codebyplan-package/src/lib/ship.ts` (the generic poller inside `codebyplan ship`). Only `pass`/`skipping` are non-blocking, `pending` waits, and every other disposition blocks (fail-safe). The two pollers MUST NOT drift — any change to this enum or its dispositions must land in both places.
343
344
  **Tools used**: `mcp__supabase__list_branches` for project_ref resolution; `mcp__supabase__get_logs` for failure diagnostics; `gh pr checks` for status polling; `supabase --experimental branches get` as CLI fallback.