codebyplan 1.13.50 → 1.13.52

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
@@ -39,7 +39,7 @@ var VERSION, PACKAGE_NAME;
39
39
  var init_version = __esm({
40
40
  "src/lib/version.ts"() {
41
41
  "use strict";
42
- VERSION = "1.13.50";
42
+ VERSION = "1.13.52";
43
43
  PACKAGE_NAME = "codebyplan";
44
44
  }
45
45
  });
@@ -14280,8 +14280,8 @@ var require_RealtimeChannel = __commonJS({
14280
14280
  }
14281
14281
  /** @internal */
14282
14282
  _notThisChannelEvent(event, ref) {
14283
- const { close, error, leave, join: join46 } = constants_1.CHANNEL_EVENTS;
14284
- const events = [close, error, leave, join46];
14283
+ const { close, error, leave, join: join48 } = constants_1.CHANNEL_EVENTS;
14284
+ const events = [close, error, leave, join48];
14285
14285
  return ref && events.includes(event) && ref !== this.joinPush.ref;
14286
14286
  }
14287
14287
  /** @internal */
@@ -37932,8 +37932,242 @@ var init_validate_waves2 = __esm({
37932
37932
  }
37933
37933
  });
37934
37934
 
37935
+ // src/cli/worktree/path.ts
37936
+ import { dirname as dirname13, basename as basename4, join as join45 } from "node:path";
37937
+ function computeWorktreePath(cwd, checkpointNumber) {
37938
+ const parent = dirname13(cwd);
37939
+ const base = basename4(cwd);
37940
+ const nnn = String(checkpointNumber).padStart(3, "0");
37941
+ return join45(parent, `${base}-CHK-${nnn}`);
37942
+ }
37943
+ var init_path = __esm({
37944
+ "src/cli/worktree/path.ts"() {
37945
+ "use strict";
37946
+ }
37947
+ });
37948
+
37949
+ // src/cli/worktree/add.ts
37950
+ import { join as join46, basename as basename5 } from "node:path";
37951
+ import { mkdir as mkdir14, readFile as readFile30, writeFile as writeFile23 } from "node:fs/promises";
37952
+ import { spawnSync as spawnSync16 } from "node:child_process";
37953
+ async function defaultGetRepoId(cwd) {
37954
+ const found = await findCodebyplanConfig(cwd);
37955
+ return found?.contents.repo_id ?? null;
37956
+ }
37957
+ async function defaultLookupBranch(repoId, checkpointNumber) {
37958
+ const checkpoints = await mcpCall("get_checkpoints", {
37959
+ repo_id: repoId
37960
+ });
37961
+ const list = Array.isArray(checkpoints) ? checkpoints : [];
37962
+ const chk = list.find((c) => c.number === checkpointNumber);
37963
+ if (!chk) {
37964
+ return {
37965
+ found: false,
37966
+ pending: false,
37967
+ message: `No checkpoint found with number ${checkpointNumber} in this repo.`
37968
+ };
37969
+ }
37970
+ if (!chk.branch_name) {
37971
+ const nnn = String(checkpointNumber).padStart(3, "0");
37972
+ return {
37973
+ found: false,
37974
+ pending: true,
37975
+ message: `Branch not yet created for CHK-${nnn} \u2014 connect this repo to GitHub in Settings \u2192 Integrations.`
37976
+ };
37977
+ }
37978
+ return { found: true, branch_name: chk.branch_name };
37979
+ }
37980
+ function defaultGitRun(args, cwd) {
37981
+ const result = spawnSync16("git", args, {
37982
+ cwd,
37983
+ encoding: "utf-8",
37984
+ stdio: ["pipe", "pipe", "pipe"]
37985
+ });
37986
+ return {
37987
+ status: result.status,
37988
+ stdout: (result.stdout ?? "").toString(),
37989
+ stderr: (result.stderr ?? "").toString(),
37990
+ error: result.error
37991
+ };
37992
+ }
37993
+ async function defaultCopyConfigStubs(srcCwd, destPath) {
37994
+ await mkdir14(join46(destPath, ".codebyplan"), { recursive: true });
37995
+ const topLevelStubs = [".mcp.json", ".env.local"];
37996
+ for (const stub of topLevelStubs) {
37997
+ try {
37998
+ const content = await readFile30(join46(srcCwd, stub), "utf-8");
37999
+ await writeFile23(join46(destPath, stub), content, "utf-8");
38000
+ } catch {
38001
+ }
38002
+ }
38003
+ try {
38004
+ const content = await readFile30(
38005
+ join46(srcCwd, ".codebyplan", "repo.json"),
38006
+ "utf-8"
38007
+ );
38008
+ await writeFile23(
38009
+ join46(destPath, ".codebyplan", "repo.json"),
38010
+ content,
38011
+ "utf-8"
38012
+ );
38013
+ } catch {
38014
+ }
38015
+ }
38016
+ async function defaultRegisterWorktree(repoId, name, path16) {
38017
+ const res = await apiPost("/worktrees", {
38018
+ repo_id: repoId,
38019
+ name,
38020
+ path: path16,
38021
+ status: "active"
38022
+ });
38023
+ return { worktree_id: res.data.id };
38024
+ }
38025
+ function parseCheckpointNumber(arg) {
38026
+ const m = /^(?:CHK-)?(\d+)$/i.exec(arg.trim());
38027
+ if (!m?.[1]) return null;
38028
+ const n = parseInt(m[1], 10);
38029
+ return isNaN(n) || n < 0 ? null : n;
38030
+ }
38031
+ async function runWorktreeAdd(args, deps = {}) {
38032
+ const rawArg = args.find((a) => !a.startsWith("--"));
38033
+ if (!rawArg) {
38034
+ process.stderr.write(
38035
+ JSON.stringify({
38036
+ error: "usage: codebyplan worktree add <CHK-NNN>"
38037
+ }) + "\n"
38038
+ );
38039
+ return 1;
38040
+ }
38041
+ const checkpointNumber = parseCheckpointNumber(rawArg);
38042
+ if (checkpointNumber === null) {
38043
+ process.stderr.write(
38044
+ JSON.stringify({
38045
+ error: `Invalid checkpoint identifier '${rawArg}'. Use CHK-NNN or a bare number (e.g. CHK-207 or 207).`
38046
+ }) + "\n"
38047
+ );
38048
+ return 1;
38049
+ }
38050
+ const cwd = deps.cwd ?? process.cwd();
38051
+ const getRepoId = deps.getRepoId ?? defaultGetRepoId;
38052
+ const lookupBranch = deps.lookupBranch ?? defaultLookupBranch;
38053
+ const gitRun = deps.gitRun ?? defaultGitRun;
38054
+ const sleep3 = deps.sleep ?? ((ms) => new Promise((resolve11) => setTimeout(resolve11, ms)));
38055
+ const copyConfigStubs = deps.copyConfigStubs ?? defaultCopyConfigStubs;
38056
+ const registerWorktree = deps.registerWorktree ?? defaultRegisterWorktree;
38057
+ try {
38058
+ const repoId = await getRepoId(cwd);
38059
+ if (!repoId) {
38060
+ process.stderr.write(
38061
+ JSON.stringify({
38062
+ error: "Could not determine repo_id. Ensure .codebyplan/repo.json exists in the project root."
38063
+ }) + "\n"
38064
+ );
38065
+ return 1;
38066
+ }
38067
+ const lookup = await lookupBranch(repoId, checkpointNumber);
38068
+ if (!lookup.found) {
38069
+ if (lookup.pending) {
38070
+ const result = {
38071
+ status: "branch_pending",
38072
+ message: lookup.message
38073
+ };
38074
+ process.stdout.write(JSON.stringify(result) + "\n");
38075
+ return 0;
38076
+ }
38077
+ process.stderr.write(JSON.stringify({ error: lookup.message }) + "\n");
38078
+ return 1;
38079
+ }
38080
+ const branchName = lookup.branch_name;
38081
+ const nnn = String(checkpointNumber).padStart(3, "0");
38082
+ gitRun(["fetch", "origin", branchName], cwd);
38083
+ let branchReady = false;
38084
+ for (let attempt = 0; attempt < BRANCH_RETRY_COUNT; attempt++) {
38085
+ if (attempt > 0) {
38086
+ await sleep3(BRANCH_RETRY_DELAY_MS);
38087
+ }
38088
+ const lsResult = gitRun(
38089
+ ["ls-remote", "--heads", "origin", branchName],
38090
+ cwd
38091
+ );
38092
+ if (lsResult.status === 0 && lsResult.stdout.trim().length > 0) {
38093
+ branchReady = true;
38094
+ break;
38095
+ }
38096
+ }
38097
+ if (!branchReady) {
38098
+ const result = {
38099
+ status: "branch_not_ready",
38100
+ message: `Branch '${branchName}' not yet visible at origin after ${BRANCH_RETRY_COUNT} retries. Connect this repo to GitHub in Settings \u2192 Integrations.`
38101
+ };
38102
+ process.stdout.write(JSON.stringify(result) + "\n");
38103
+ return 1;
38104
+ }
38105
+ const worktreePath = computeWorktreePath(cwd, checkpointNumber);
38106
+ const worktreeName = `${basename5(cwd)}-CHK-${nnn}`;
38107
+ const addResult = gitRun(
38108
+ ["worktree", "add", worktreePath, branchName],
38109
+ cwd
38110
+ );
38111
+ if (addResult.status !== 0) {
38112
+ const errMsg = addResult.stderr.trim() || addResult.error?.message || "unknown error";
38113
+ process.stderr.write(
38114
+ JSON.stringify({ error: `git worktree add failed: ${errMsg}` }) + "\n"
38115
+ );
38116
+ return 1;
38117
+ }
38118
+ try {
38119
+ await copyConfigStubs(cwd, worktreePath);
38120
+ } catch (err) {
38121
+ const msg = err instanceof Error ? err.message : String(err);
38122
+ process.stderr.write(`Warning: failed to copy config stubs: ${msg}
38123
+ `);
38124
+ }
38125
+ try {
38126
+ const { worktree_id } = await registerWorktree(
38127
+ repoId,
38128
+ worktreeName,
38129
+ worktreePath
38130
+ );
38131
+ const result = {
38132
+ status: "added",
38133
+ worktree_path: worktreePath,
38134
+ branch_name: branchName,
38135
+ worktree_id
38136
+ };
38137
+ process.stdout.write(JSON.stringify(result) + "\n");
38138
+ return 0;
38139
+ } catch (err) {
38140
+ const msg = err instanceof Error ? err.message : String(err);
38141
+ const result = {
38142
+ status: "added",
38143
+ worktree_path: worktreePath,
38144
+ branch_name: branchName,
38145
+ warn: `Worktree created but registration failed: ${msg}`
38146
+ };
38147
+ process.stdout.write(JSON.stringify(result) + "\n");
38148
+ return 0;
38149
+ }
38150
+ } catch (err) {
38151
+ const msg = err instanceof Error ? err.message : String(err);
38152
+ process.stderr.write(JSON.stringify({ error: msg }) + "\n");
38153
+ return 1;
38154
+ }
38155
+ }
38156
+ var BRANCH_RETRY_COUNT, BRANCH_RETRY_DELAY_MS;
38157
+ var init_add = __esm({
38158
+ "src/cli/worktree/add.ts"() {
38159
+ "use strict";
38160
+ init_path();
38161
+ init_flags();
38162
+ init_mcp_client();
38163
+ init_api();
38164
+ BRANCH_RETRY_COUNT = 3;
38165
+ BRANCH_RETRY_DELAY_MS = 2e3;
38166
+ }
38167
+ });
38168
+
37935
38169
  // src/cli/worktree/create.ts
37936
- import { join as join45 } from "node:path";
38170
+ import { join as join47 } from "node:path";
37937
38171
  async function defaultGetRepoIdentity(cwd) {
37938
38172
  const found = await findCodebyplanConfig(cwd);
37939
38173
  const contents = found?.contents ?? null;
@@ -37945,7 +38179,7 @@ async function defaultGetRepoIdentity(cwd) {
37945
38179
  project_id: typeof contents?.["project_id"] === "string" ? contents["project_id"] : void 0
37946
38180
  };
37947
38181
  }
37948
- async function defaultRegisterWorktree(repoId, name, path16) {
38182
+ async function defaultRegisterWorktree2(repoId, name, path16) {
37949
38183
  const res = await apiPost("/worktrees", {
37950
38184
  repo_id: repoId,
37951
38185
  name,
@@ -37977,7 +38211,7 @@ async function runWorktreeCreate(args, deps = {}) {
37977
38211
  const cwd = deps.cwd ?? process.cwd();
37978
38212
  const getRepoIdentity = deps.getRepoIdentity ?? defaultGetRepoIdentity;
37979
38213
  const getDeviceId = deps.getDeviceId ?? getOrCreateDeviceId;
37980
- const registerWorktree = deps.registerWorktree ?? defaultRegisterWorktree;
38214
+ const registerWorktree = deps.registerWorktree ?? defaultRegisterWorktree2;
37981
38215
  const identity = await getRepoIdentity(cwd);
37982
38216
  const repoId = identity?.repo_id ?? null;
37983
38217
  if (!identity || !repoId) {
@@ -37988,7 +38222,7 @@ async function runWorktreeCreate(args, deps = {}) {
37988
38222
  );
37989
38223
  return 1;
37990
38224
  }
37991
- const worktreePath = explicitPath ?? join45(cwd, "..", name);
38225
+ const worktreePath = explicitPath ?? join47(cwd, "..", name);
37992
38226
  const deviceId = await getDeviceId(cwd);
37993
38227
  let filesWritten = false;
37994
38228
  try {
@@ -38044,7 +38278,9 @@ var init_create = __esm({
38044
38278
  });
38045
38279
 
38046
38280
  // src/cli/worktree/remove.ts
38047
- async function defaultGetRepoId(cwd) {
38281
+ import { basename as basename6 } from "node:path";
38282
+ import { spawnSync as spawnSync17 } from "node:child_process";
38283
+ async function defaultGetRepoId2(cwd) {
38048
38284
  const found = await findCodebyplanConfig(cwd);
38049
38285
  return found?.contents.repo_id ?? null;
38050
38286
  }
@@ -38064,6 +38300,192 @@ async function defaultFindWorktreeId(repoId, name) {
38064
38300
  async function defaultDeregisterWorktree(worktreeId) {
38065
38301
  await mcpCall("delete_worktree", { worktree_id: worktreeId });
38066
38302
  }
38303
+ function defaultGitRun2(args, cwd) {
38304
+ const result = spawnSync17("git", args, {
38305
+ cwd,
38306
+ encoding: "utf-8",
38307
+ stdio: ["pipe", "pipe", "pipe"]
38308
+ });
38309
+ return {
38310
+ status: result.status,
38311
+ stdout: (result.stdout ?? "").toString(),
38312
+ stderr: (result.stderr ?? "").toString(),
38313
+ error: result.error
38314
+ };
38315
+ }
38316
+ function defaultGetWorktreeBranchName(worktreePath) {
38317
+ const result = spawnSync17("git", ["symbolic-ref", "--short", "HEAD"], {
38318
+ cwd: worktreePath,
38319
+ encoding: "utf-8",
38320
+ stdio: ["pipe", "pipe", "pipe"]
38321
+ });
38322
+ if (result.status === 0 && result.stdout) {
38323
+ return result.stdout.trim() || null;
38324
+ }
38325
+ return null;
38326
+ }
38327
+ async function defaultDeleteSupabaseBranch(branchName, cwd) {
38328
+ const token = process.env.SUPABASE_ACCESS_TOKEN;
38329
+ if (!token) {
38330
+ return {
38331
+ deleted: false,
38332
+ warn: "SUPABASE_ACCESS_TOKEN not set \u2014 skipping Supabase preview-branch teardown."
38333
+ };
38334
+ }
38335
+ const shipment = readShipmentConfig(cwd);
38336
+ const parentRef = shipment.parentRef;
38337
+ if (!parentRef) {
38338
+ return {
38339
+ deleted: false,
38340
+ warn: "Supabase project_ref not configured in .codebyplan/shipment.json \u2014 skipping teardown."
38341
+ };
38342
+ }
38343
+ const gitCfg = readGitConfig(cwd);
38344
+ if (branchName === gitCfg.production || gitCfg.protected.includes(branchName)) {
38345
+ return {
38346
+ deleted: false,
38347
+ warn: `Skipping Supabase teardown for protected branch '${branchName}'.`
38348
+ };
38349
+ }
38350
+ if (gitCfg.integration !== null && branchName === gitCfg.integration) {
38351
+ return {
38352
+ deleted: false,
38353
+ warn: `Skipping Supabase teardown for integration branch '${branchName}'.`
38354
+ };
38355
+ }
38356
+ try {
38357
+ const listRes = await fetch(
38358
+ `https://api.supabase.com/v1/projects/${parentRef}/branches`,
38359
+ {
38360
+ headers: {
38361
+ Authorization: `Bearer ${token}`,
38362
+ "Content-Type": "application/json"
38363
+ }
38364
+ }
38365
+ );
38366
+ if (!listRes.ok) {
38367
+ return {
38368
+ deleted: false,
38369
+ warn: `Supabase list_branches failed (HTTP ${listRes.status}) \u2014 verify the branch manually in the dashboard.`
38370
+ };
38371
+ }
38372
+ const branches = await listRes.json();
38373
+ if (!Array.isArray(branches)) {
38374
+ return {
38375
+ deleted: false,
38376
+ warn: "Supabase list_branches returned unexpected shape \u2014 verify manually in the dashboard."
38377
+ };
38378
+ }
38379
+ const match = branches.find((b) => b.name === branchName);
38380
+ if (!match) {
38381
+ return { deleted: true };
38382
+ }
38383
+ if (match.is_default === true) {
38384
+ return {
38385
+ deleted: false,
38386
+ warn: `Refusing to delete is_default Supabase branch '${branchName}'.`
38387
+ };
38388
+ }
38389
+ if (!match.id) {
38390
+ return {
38391
+ deleted: false,
38392
+ warn: `Supabase branch '${branchName}' found but has no id \u2014 skipping.`
38393
+ };
38394
+ }
38395
+ const deleteRes = await fetch(
38396
+ `https://api.supabase.com/v1/projects/${parentRef}/branches/${match.id}`,
38397
+ {
38398
+ method: "DELETE",
38399
+ headers: { Authorization: `Bearer ${token}` }
38400
+ }
38401
+ );
38402
+ if (!deleteRes.ok) {
38403
+ return {
38404
+ deleted: false,
38405
+ warn: `Supabase delete_branch failed (HTTP ${deleteRes.status}) \u2014 verify the branch manually in the dashboard.`
38406
+ };
38407
+ }
38408
+ return { deleted: true };
38409
+ } catch (err) {
38410
+ const msg = err instanceof Error ? err.message : String(err);
38411
+ return {
38412
+ deleted: false,
38413
+ warn: `Supabase teardown error: ${msg} \u2014 verify manually in the dashboard.`
38414
+ };
38415
+ }
38416
+ }
38417
+ function parseChkNumber(name) {
38418
+ const chkMatch = /^CHK-(\d+)$/i.exec(name);
38419
+ if (chkMatch?.[1]) {
38420
+ const n = parseInt(chkMatch[1], 10);
38421
+ return isNaN(n) ? null : n;
38422
+ }
38423
+ if (/^\d+$/.test(name)) {
38424
+ const n = parseInt(name, 10);
38425
+ return isNaN(n) ? null : n;
38426
+ }
38427
+ return null;
38428
+ }
38429
+ async function runWorktreeRemoveChk(checkpointNumber, deps) {
38430
+ const cwd = deps.cwd ?? process.cwd();
38431
+ const getRepoId = deps.getRepoId ?? defaultGetRepoId2;
38432
+ const findWorktreeId = deps.findWorktreeId ?? defaultFindWorktreeId;
38433
+ const deregisterWorktree = deps.deregisterWorktree ?? defaultDeregisterWorktree;
38434
+ const gitRun = deps.gitRun ?? defaultGitRun2;
38435
+ const getWorktreeBranchName = deps.getWorktreeBranchName ?? defaultGetWorktreeBranchName;
38436
+ const deleteSupabaseBranch = deps.deleteSupabaseBranch ?? defaultDeleteSupabaseBranch;
38437
+ const nnn = String(checkpointNumber).padStart(3, "0");
38438
+ const worktreeName = `${basename6(cwd)}-CHK-${nnn}`;
38439
+ const worktreePath = computeWorktreePath(cwd, checkpointNumber);
38440
+ const warnings = [];
38441
+ const branchName = getWorktreeBranchName(worktreePath);
38442
+ const removeResult = gitRun(["worktree", "remove", worktreePath], cwd);
38443
+ const git_removed = removeResult.status === 0;
38444
+ if (!git_removed) {
38445
+ const errMsg = removeResult.stderr.trim() || removeResult.error?.message || "unknown error";
38446
+ warnings.push(`git worktree remove failed: ${errMsg}`);
38447
+ }
38448
+ let mcp_deregistered = false;
38449
+ const repoId = await getRepoId(cwd);
38450
+ if (repoId) {
38451
+ const worktreeId = await findWorktreeId(repoId, worktreeName);
38452
+ if (worktreeId) {
38453
+ try {
38454
+ await deregisterWorktree(worktreeId);
38455
+ mcp_deregistered = true;
38456
+ } catch (err) {
38457
+ const msg = err instanceof Error ? err.message : String(err);
38458
+ warnings.push(`MCP deregistration failed: ${msg}`);
38459
+ }
38460
+ } else {
38461
+ warnings.push(
38462
+ `No registered worktree found with name '${worktreeName}' for this repo.`
38463
+ );
38464
+ }
38465
+ } else {
38466
+ warnings.push("Could not determine repo_id \u2014 worktree not deregistered.");
38467
+ }
38468
+ let supabase_torn_down = false;
38469
+ if (branchName) {
38470
+ const teardown = await deleteSupabaseBranch(branchName, cwd);
38471
+ supabase_torn_down = teardown.deleted;
38472
+ if (teardown.warn) {
38473
+ warnings.push(teardown.warn);
38474
+ }
38475
+ } else {
38476
+ warnings.push(
38477
+ `Could not read the branch name for '${worktreePath}' (directory may already be deleted) \u2014 the Supabase preview branch was NOT torn down. Delete it manually in the Supabase dashboard if it still exists.`
38478
+ );
38479
+ }
38480
+ const result = {
38481
+ mcp_deregistered,
38482
+ git_removed,
38483
+ supabase_torn_down,
38484
+ ...warnings.length > 0 ? { warn: warnings.join("; ") } : {}
38485
+ };
38486
+ process.stdout.write(JSON.stringify(result) + "\n");
38487
+ return 0;
38488
+ }
38067
38489
  async function runWorktreeRemove(args, deps = {}) {
38068
38490
  const name = args.find((a) => !a.startsWith("--"));
38069
38491
  if (!name) {
@@ -38074,8 +38496,12 @@ async function runWorktreeRemove(args, deps = {}) {
38074
38496
  );
38075
38497
  return 1;
38076
38498
  }
38499
+ const chkNumber = parseChkNumber(name);
38500
+ if (chkNumber !== null) {
38501
+ return runWorktreeRemoveChk(chkNumber, deps);
38502
+ }
38077
38503
  const cwd = deps.cwd ?? process.cwd();
38078
- const getRepoId = deps.getRepoId ?? defaultGetRepoId;
38504
+ const getRepoId = deps.getRepoId ?? defaultGetRepoId2;
38079
38505
  const findWorktreeId = deps.findWorktreeId ?? defaultFindWorktreeId;
38080
38506
  const deregisterWorktree = deps.deregisterWorktree ?? defaultDeregisterWorktree;
38081
38507
  const repoId = await getRepoId(cwd);
@@ -38114,9 +38540,11 @@ async function runWorktreeRemove(args, deps = {}) {
38114
38540
  var init_remove = __esm({
38115
38541
  "src/cli/worktree/remove.ts"() {
38116
38542
  "use strict";
38543
+ init_path();
38117
38544
  init_api();
38118
38545
  init_mcp_client();
38119
38546
  init_flags();
38547
+ init_supabase();
38120
38548
  }
38121
38549
  });
38122
38550
 
@@ -38127,6 +38555,9 @@ __export(worktree_exports, {
38127
38555
  });
38128
38556
  async function runWorktreeCommand(args) {
38129
38557
  const subcommand = args[0];
38558
+ if (subcommand === "add") {
38559
+ return runWorktreeAdd(args.slice(1));
38560
+ }
38130
38561
  if (subcommand === "create") {
38131
38562
  return runWorktreeCreate(args.slice(1));
38132
38563
  }
@@ -38140,7 +38571,7 @@ async function runWorktreeCommand(args) {
38140
38571
  if (subcommand) {
38141
38572
  process.stderr.write(
38142
38573
  `Unknown subcommand: codebyplan worktree ${subcommand}
38143
- Available: create, remove
38574
+ Available: add, create, remove
38144
38575
  `
38145
38576
  );
38146
38577
  return 1;
@@ -38150,12 +38581,13 @@ Available: create, remove
38150
38581
  }
38151
38582
  function printWorktreeHelp() {
38152
38583
  process.stdout.write(
38153
- "\n codebyplan worktree <subcommand>\n\n Subcommands:\n create <name> Write .codebyplan/ files and register the worktree in CodeByPlan\n remove <name> Deregister a worktree from CodeByPlan\n\n create flags:\n --path <abs> Explicit absolute path to the worktree root (primary mechanism)\n\n Output is JSON on stdout. Exit 0 on success or non-fatal failure; exit 1 on usage error.\n\n"
38584
+ "\n codebyplan worktree <subcommand>\n\n Subcommands:\n add <CHK-NNN> Create a per-checkpoint git worktree on the checkpoint's feat branch and register it\n create <name> Write .codebyplan/ files and register the worktree in CodeByPlan\n remove <name> Deregister a worktree (or `remove CHK-NNN` to prune the per-checkpoint worktree + Supabase preview)\n\n create flags:\n --path <abs> Explicit absolute path to the worktree root (primary mechanism)\n\n Output is JSON on stdout. Exit 0 on success or non-fatal failure; exit 1 on usage error.\n\n"
38154
38585
  );
38155
38586
  }
38156
38587
  var init_worktree2 = __esm({
38157
38588
  "src/cli/worktree.ts"() {
38158
38589
  "use strict";
38590
+ init_add();
38159
38591
  init_create();
38160
38592
  init_remove();
38161
38593
  }
@@ -38275,7 +38707,7 @@ var init_e2e = __esm({
38275
38707
  });
38276
38708
 
38277
38709
  // src/cli/e2e/verify-round.ts
38278
- import { readFile as readFile30 } from "node:fs/promises";
38710
+ import { readFile as readFile31 } from "node:fs/promises";
38279
38711
  import { resolve as resolve9 } from "node:path";
38280
38712
  async function defaultFetchRounds(taskId) {
38281
38713
  return mcpCall("get_rounds", { task_id: taskId });
@@ -38283,7 +38715,7 @@ async function defaultFetchRounds(taskId) {
38283
38715
  async function defaultReadE2eConfig(cwd) {
38284
38716
  try {
38285
38717
  const p = resolve9(cwd, ".codebyplan", "e2e.json");
38286
- const raw = await readFile30(p, "utf-8");
38718
+ const raw = await readFile31(p, "utf-8");
38287
38719
  return JSON.parse(raw);
38288
38720
  } catch {
38289
38721
  return null;
@@ -39063,7 +39495,7 @@ void (async () => {
39063
39495
  codebyplan claude Claude asset management (install/update/uninstall/generate/migrate-memory)
39064
39496
  codebyplan supabase Preview-branch helpers (resolve-preview/teardown-preview/new-migration/preview-check)
39065
39497
  codebyplan session Session helpers (home-ff/freshness-gate/infra-files)
39066
- codebyplan worktree Worktree management (create/remove)
39498
+ codebyplan worktree Worktree management (add/create/remove)
39067
39499
  codebyplan e2e E2E deterministic gates (verify-round)
39068
39500
  codebyplan statusline Show or set the statusline renderer (bash/node/python)
39069
39501
  codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.50",
3
+ "version": "1.13.52",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,7 +7,6 @@ paths:
7
7
  - ".claude/skills/cbp-supabase-migrate/**"
8
8
  - ".claude/skills/cbp-supabase-branch-check/**"
9
9
  - ".claude/skills/cbp-checkpoint-end/**"
10
- - ".claude/skills/cbp-git-worktree-remove/**"
11
10
  - ".claude/skills/cbp-ship-main/**"
12
11
  - ".claude/skills/cbp-standalone-task-complete/**"
13
12
  ---
@@ -48,7 +47,7 @@ The Supabase branch is removed wherever the git branch is deleted:
48
47
  | Skill | Trigger |
49
48
  |---|---|
50
49
  | `cbp-checkpoint-end` | stale-branch cleanup + current feat-branch delete on ship |
51
- | `cbp-git-worktree-remove` | worktree teardown removes the coupled Supabase branch |
50
+ | `codebyplan worktree remove CHK-NNN` | worktree teardown removes the coupled Supabase branch |
52
51
  | `cbp-ship-main` | `branch_deleted` event after PR merge |
53
52
  | `cbp-standalone-task-complete` | `branch_deleted` event after standalone PR merge (Step 7.3) |
54
53
 
@@ -94,7 +93,7 @@ or auto-created by the GitHub integration — both paths use the same branch nam
94
93
  | Role | Skill |
95
94
  |---|---|
96
95
  | Create (lazy) | `cbp-supabase-migrate` (Step 2.3) |
97
- | Delete | `cbp-checkpoint-end`, `cbp-standalone-task-complete`, `cbp-git-worktree-remove`, `cbp-ship-main` |
96
+ | Delete | `cbp-checkpoint-end`, `cbp-standalone-task-complete`, `codebyplan worktree remove`, `cbp-ship-main` |
98
97
  | PR gate | `cbp-supabase-branch-check` |
99
98
 
100
99
  Each skill in the Skill Map above carries an inline back-reference to this rule at its create or teardown step.
@@ -123,8 +123,6 @@
123
123
  "Skill(cbp-frontend-ux)",
124
124
  "Skill(cbp-git-branch-feat-create)",
125
125
  "Skill(cbp-git-commit)",
126
- "Skill(cbp-git-worktree-create)",
127
- "Skill(cbp-git-worktree-remove)",
128
126
  "Skill(cbp-map-architecture)",
129
127
  "Skill(cbp-merge-main)",
130
128
  "Skill(cbp-refresh-arch-map)",
@@ -32,7 +32,7 @@ Omit `model:` entirely (inherit the session model). Set `effort:` per the skill'
32
32
  | `xhigh` | deep authoring / planning / orchestration (most skills) | build-cc-*, checkpoint-check/end/plan, round-start/input/execute, task-create/start/complete/testing, standalone-task-*, frontend-*, ship, ship-configure, supabase-*, setup-e2e/eslint, session-end |
33
33
  | `high` | summary / orchestration, lighter than xhigh | checkpoint-create, checkpoint-start, checkpoint-update, round-end, task-check, standalone-task-check, ship-main, merge-main |
34
34
  | `medium` | moderate, scoped sync work | refresh-infra |
35
- | `low` | pure mechanical / dispatch / templated work | checkpoint-complete, round-check, round-complete, round-update, todo, session-start, git-commit, git-branch-feat-create, git-worktree-create, git-worktree-remove |
35
+ | `low` | pure mechanical / dispatch / templated work | checkpoint-complete, round-check, round-complete, round-update, todo, session-start, git-commit, git-branch-feat-create |
36
36
 
37
37
  A skill that carries a `model:` line is a **gap** — remove it unless a deliberate, documented exception is recorded here (currently none).
38
38
 
@@ -157,5 +157,6 @@ Checks: name matches directory, frontmatter parses, no invalid fields, arg-hint
157
157
  - Rendered `SKILL.md` content enters the conversation once when invoked and stays until compaction — write standing instructions, not one-time steps
158
158
  - Preloaded subagent skills cannot have `disable-model-invocation: true`
159
159
  - `context: fork` is only meaningful when the skill body contains an actionable task; otherwise the subagent receives guidelines with no prompt
160
+ - `context: fork` ALSO disqualifies fan-out / spawn-then-route / inline-by-design / consumed-inline skills — they run in the main context for a reason; see [reference/fork-eligibility.md](reference/fork-eligibility.md) for the eligibility test + the CBP classification of which skills may fork and which must stay inline
160
161
  - Descriptions front-load key use case: `description` + `when_to_use` cap at 1,536 chars in the skill listing
161
162
  - `paths:` scoping applies when Claude reads files matching the globs, not on every tool use
@@ -0,0 +1,78 @@
1
+ # `context: fork` Eligibility — when to fork a skill, and when not to
2
+
3
+ `context: fork` runs the **entire SKILL.md body in a forked subagent** — it inherits the
4
+ parent conversation and, per the runtime, **runs in the background by default**. It is
5
+ isolation for a *whole skill*, not a way to delegate one sub-step. A forked body therefore
6
+ cannot drive the main pipeline: it can't `AskUserQuestion`, can't auto-trigger another
7
+ skill, and can't run an inline-fallback that the orchestrator depends on.
8
+
9
+ So forking only helps a narrow shape of skill. The canonical eligible example is
10
+ [examples/fork-skill.md](../examples/fork-skill.md): a single self-contained analytical task
11
+ that reads, reasons, and returns a summary — nothing else.
12
+
13
+ ## Eligibility test
14
+
15
+ A skill is **fork-eligible** only when ALL hold:
16
+
17
+ 1. Its whole body is **one** self-contained analytical/generative task (read → reason → return).
18
+ 2. It is **non-interactive** — no `AskUserQuestion`, no user decision gates.
19
+ 3. It does **not route** — no auto-trigger of another skill, no close-out directive that must
20
+ fire in the main context.
21
+ 4. It does **not fan out** — it does not spawn multiple subagents and coordinate them.
22
+ 5. It has **no inline-fallback** contract the orchestrator relies on.
23
+
24
+ Fail any one → the skill stays **inline** (main context). Inline skills still get clean
25
+ context isolation the right way: by delegating their heavy step to a dedicated **agent**
26
+ (e.g. `cbp-task-check`, `cbp-improve-round`, `cbp-round-executor`). The agent is the
27
+ isolation boundary; the skill stays in the main thread to orchestrate, route, and interact.
28
+
29
+ ## When NOT to use `context: fork` (the disqualifying patterns)
30
+
31
+ | Pattern | Why it can't fork | Example skills |
32
+ |---------|-------------------|----------------|
33
+ | **fan-out** | spawns multiple agents in parallel and coordinates them | `cbp-round-execute`, `cbp-checkpoint-check`, `cbp-map-architecture`, `cbp-refresh-arch-map` |
34
+ | **spawn-then-route** | spawns one agent, then `AskUserQuestion` / auto-triggers the next skill / runs inline-fallback | `cbp-task-check`, `cbp-standalone-task-check`, `cbp-round-start`, `cbp-round-end`, `cbp-checkpoint-plan` |
35
+ | **inline-by-design** | interactive Q&A or stepwise writes that must stay in the main context | `cbp-task-create`, `cbp-task-complete`, `cbp-round-update`, `cbp-merge-main` |
36
+ | **consumed-inline** | invoked *by* an agent (e.g. round-executor) and applies fixes synchronously into that context | `cbp-frontend-design`, `cbp-frontend-ui`, `cbp-frontend-ux` |
37
+ | **doc-ref-only** | mentions subagents/fork only as documentation; runs inline authoring | the `cbp-build-cc-*` authoring skills, `cbp-supabase-migrate` |
38
+
39
+ ## CHK-220 audit — classification matrix
40
+
41
+ Every skill whose `SKILL.md` touches the subagent/fork boundary — by spawning a subagent, by
42
+ being invoked inline by an agent, or by documenting the feature — was classified against the
43
+ eligibility test. **Result: 0 of 25 are fork-eligible** — none were migrated, because every
44
+ one either already isolates heavy work in a dedicated agent (the correct boundary) or depends
45
+ on inline orchestration/interaction that a background fork would break.
46
+
47
+ | Skill | Pattern | Fork-eligible |
48
+ |-------|---------|:---:|
49
+ | cbp-round-execute | fan-out | no |
50
+ | cbp-checkpoint-check | fan-out | no |
51
+ | cbp-map-architecture | fan-out | no |
52
+ | cbp-refresh-arch-map | fan-out | no |
53
+ | cbp-round-start | spawn-then-route | no |
54
+ | cbp-round-end | spawn-then-route | no |
55
+ | cbp-task-check | spawn-then-route | no |
56
+ | cbp-standalone-task-check | spawn-then-route | no |
57
+ | cbp-checkpoint-plan | spawn-then-route | no |
58
+ | cbp-round-update | inline-by-design | no |
59
+ | cbp-task-create | inline-by-design | no |
60
+ | cbp-standalone-task-create | inline-by-design | no |
61
+ | cbp-task-complete | inline-by-design | no |
62
+ | cbp-standalone-task-complete | inline-by-design | no |
63
+ | cbp-merge-main | inline-by-design | no |
64
+ | cbp-task-testing | inline-by-design | no |
65
+ | cbp-standalone-task-testing | inline-by-design | no |
66
+ | cbp-frontend-design | consumed-inline | no |
67
+ | cbp-frontend-ui | consumed-inline | no |
68
+ | cbp-frontend-ux | consumed-inline | no |
69
+ | cbp-supabase-migrate | doc-ref-only | no |
70
+ | cbp-build-cc-skill | doc-ref-only | no |
71
+ | cbp-build-cc-agent | doc-ref-only | no |
72
+ | cbp-build-cc-settings | doc-ref-only | no |
73
+ | cbp-build-cc-mode | doc-ref-only | no |
74
+
75
+ If a **new** skill is authored that passes the eligibility test above, fork it: add
76
+ `context: fork` + `agent: <Explore|Plan|general-purpose|custom>` (use `--fork` in
77
+ `/cbp-build-cc-skill`). Do not retrofit fork onto any skill in the table — their inline
78
+ shape is load-bearing.
@@ -20,6 +20,10 @@ Source: official Claude Code skills spec. All fields are optional; `description`
20
20
  | `paths` | Globs that auto-load the skill when matching files are in play |
21
21
  | `shell` | `bash` (default) or `powershell` for `!`-commands |
22
22
 
23
+ > `context: fork` / `agent` are right for only a narrow shape of skill — a single
24
+ > non-interactive analytical body. See [fork-eligibility.md](fork-eligibility.md) for the
25
+ > eligibility test and the CBP classification of which skills may fork (and which must stay inline).
26
+
23
27
  ## Character budget
24
28
 
25
29
  Combined `description` + `when_to_use` truncates at **1,536 characters** in the skill listing. Front-load the key use case.
@@ -113,6 +113,7 @@ Persist the branch via `codebyplan checkpoint update --id <checkpoint-id> --bran
113
113
 
114
114
  **CHK-NNN**: [title] • **Deadline**: [date] • **Branch**: feat/CHK-NNN-slug
115
115
  **Claim**: [claimed by this worktree / left open]
116
+ **Worktree**: `npx codebyplan worktree add CHK-{NNN}`
116
117
 
117
118
  Now planning CHK-NNN… handing off to /cbp-checkpoint-plan.
118
119
  ```
@@ -72,6 +72,7 @@ Show a one-line confirmation before routing:
72
72
 
73
73
  **CHK-NNN**: [title] • **Status**: active • **Claimed by**: [worktree or "open"]
74
74
  **Next task**: TASK-[N] — [title]
75
+ **Worktree**: `npx codebyplan worktree add CHK-{NNN}`
75
76
  ```
76
77
 
77
78
  ## Integration
@@ -74,23 +74,9 @@ Same rule as `/cbp-session-start` Step 5.7 — only commit files that are **not*
74
74
 
75
75
  Non-blocking — session end proceeds either way.
76
76
 
77
- ### Step 1.6: Home-Branch Fast-Forward
78
-
79
- Keep this worktree's **home branch** rooted at the freshest production tip — no prompt, fully non-blocking. Home branches are the folder-named placeholder branches each worktree rests on (e.g. `codebyplan-web`); `feat/*` branches are deliberately skipped — they integrate via `/cbp-merge-main` with QA, never an auto fast-forward.
80
-
81
- Runs after Step 1.5 so any infra commits land first, and before Step 1.7 so the freshness gate's asset update operates on the freshest tree.
82
-
83
- Run `codebyplan session home-ff` and parse the JSON output (`{ result: 'skipped'|'warn'|'fast_forwarded', reason?, warn? }`):
84
-
85
- - **`result === 'skipped'`** (non-home branch or production mismatch): proceed silently.
86
- - **`result === 'warn'`** (fast-forward attempt failed): surface the `warn` field as one line, then proceed silently.
87
- - **`result === 'fast_forwarded'`** (home branch updated): proceed silently.
88
-
89
- Never rebase, reset, force-push, or stash. A non-fast-forwardable home branch is a signal to reconcile manually, not to overwrite.
90
-
91
77
  ### Step 1.7: Package Freshness Gate
92
78
 
93
- Check whether a newer `codebyplan` is published and safe to auto-install on this worktree's current branch, then run the local install + asset update. Runs after the Step 1.6 home-branch fast-forward (so any update lands on the freshest tree) and before Step 2. Fully non-blocking — the guard and skip branches proceed silently; any failure in the install/update commands warns once and continues to Step 2. session-end never halts.
79
+ Check whether a newer `codebyplan` is published and safe to auto-install on this worktree's current branch, then run the local install + asset update. Runs after the Step 1.5 non-task-file commit and before Step 2. Fully non-blocking — the guard and skip branches proceed silently; any failure in the install/update commands warns once and continues to Step 2. session-end never halts.
94
80
 
95
81
  Run `codebyplan session freshness-gate` (WITHOUT `--halt-on-update`; session-end is continue-only) and parse the JSON output (`{ result: 'skipped'|'guarded'|'up_to_date'|'updated'|'error', ... }`):
96
82
 
@@ -110,8 +96,6 @@ Run `codebyplan session freshness-gate` (WITHOUT `--halt-on-update`; session-end
110
96
  On `yes`: `git add` the listed paths only (not the whole directories), then trigger `/cbp-git-commit`. On `no`: skip. On `select`: ask which subset.
111
97
  - Continue to Step 2 — session-end does NOT halt. The update lands in the current worktree on its current branch; main/protected branches and the canonical source repo are already excluded by the `result !== 'guarded'` check above.
112
98
 
113
- **Home branches are intentionally not guarded.** A worktree's folder-named home branch (e.g. `codebyplan-web`) is neither protected/main nor the canonical source, so the freshness gate returns `result !== 'guarded'` there and this gate runs the update exactly as on a feat branch — landing it on whichever branch the worktree currently rests on (the deliberate "update the branch they're on, except main/canonical" behavior).
114
-
115
99
  Non-blocking — session end proceeds regardless of outcome.
116
100
 
117
101
  ### Step 2: Auto-Stop Servers
@@ -149,7 +133,7 @@ You can close this window.
149
133
  ## Integration
150
134
 
151
135
  - **Triggered by**: user invocation (prompted by `/cbp-todo` when no work remains)
152
- - **Reads**: `.codebyplan/repo.json`, `.codebyplan/git.json` (`branch_config.production` for the Step 1.6 home-branch fast-forward); local-first reads (with `npx codebyplan sync` + MCP break-glass): `.codebyplan/state/session/current.json` (Step 1 resolve log), `.codebyplan/state/todos.json` (Step 1.3 handoff snapshot + Step 1.5 active-task lookup), `.codebyplan/state/checkpoints/<id>/tasks/<id>/rounds/` (Step 1.5 task-file resolution); `codebyplan session home-ff` (Step 1.6 home-branch fast-forward); `codebyplan session freshness-gate` (Step 1.7 package-freshness gate, without --halt-on-update); `codebyplan session infra-files --json --task-files <csv>` (Step 1.5 infra-file set math)
136
+ - **Reads**: `.codebyplan/repo.json`; local-first reads (with `npx codebyplan sync` + MCP break-glass): `.codebyplan/state/session/current.json` (Step 1 resolve log), `.codebyplan/state/todos.json` (Step 1.3 handoff snapshot + Step 1.5 active-task lookup), `.codebyplan/state/checkpoints/<id>/tasks/<id>/rounds/` (Step 1.5 task-file resolution); `codebyplan session freshness-gate` (Step 1.7 package-freshness gate, without --halt-on-update); `codebyplan session infra-files --json --task-files <csv>` (Step 1.5 infra-file set math)
153
137
  - **Writes**: `codebyplan session update-log --id <id> ...` (Step 1 finalize — CLI write-through to `.codebyplan/state/session/current.json`; break-glass: MCP `update_session_log`), `codebyplan session create-log` (Step 1 fallback when no log exists; break-glass: MCP `create_session_log`), `codebyplan session update-state --action deactivate` (Step 3 — CLI write-through to `.codebyplan/state/session/state.json`; break-glass: MCP `update_session_state`)
154
138
  - **Spawns**: none
155
139
  - **Triggers**: none at the skill-contract level. Step 1.5 may invoke `/cbp-git-commit` inline on user approval; Step 1.7 may invoke `/cbp-git-commit` on the `result === 'updated'` path (committing changed `.claude/` and `.codebyplan/` paths).
@@ -11,7 +11,7 @@ Activate the session, open a fresh session log, and surface the previous log's p
11
11
 
12
12
  ## Instructions
13
13
 
14
- Run Steps 0 through 5.8 silently (no intermediate output) — except Step 0 aborts the session on MCP failure, Step 1.4 may surface a one-line fast-forward note or warning, Step 1.5 may surface a one-line infra-drift nudge, Step 1.55 may surface a one-line architecture-map drift nudge, Step 1.6 may run an install-and-halt path, Step 1.7 may surface a one-line LSP binary nudge, Step 4.5 may auto-resume a handoff and exit session-start entirely (no Step 6 output), and Step 5.7 may surface an approval gate. (Step numbers are organizational labels; execution order is 0 → 1 → 1.4 → 1.5 → 1.55 → 1.6 → 1.7 → 3 → 4 → 4.5 → 5 → 5.7 → 5.8 → 6 → 7.) Produce ONE output block at Step 6, then auto-trigger or stop per Step 7.
14
+ Run Steps 0 through 5.8 silently (no intermediate output) — except Step 0 aborts the session on MCP failure, Step 1.5 may surface a one-line infra-drift nudge, Step 1.55 may surface a one-line architecture-map drift nudge, Step 1.6 may run an install-and-halt path, Step 1.7 may surface a one-line LSP binary nudge, Step 4.5 may auto-resume a handoff and exit session-start entirely (no Step 6 output), and Step 5.7 may surface an approval gate. (Step numbers are organizational labels; execution order is 0 → 1 → 1.5 → 1.55 → 1.6 → 1.7 → 3 → 4 → 4.5 → 5 → 5.7 → 5.8 → 6 → 7.) Produce ONE output block at Step 6, then auto-trigger or stop per Step 7.
15
15
 
16
16
  ### Step 0: MCP Health Gate
17
17
 
@@ -50,23 +50,11 @@ Extract `worktree_id` and `error_kind` from the JSON output.
50
50
 
51
51
  Pass `WORKTREE_ID` to MCP tools that support it. Null `WORKTREE_ID` means the (device, path, branch) tuple is unregistered — note this for Step 6.
52
52
 
53
- ### Step 1.4: Home-Branch Fast-Forward
54
-
55
- Keep this worktree's **home branch** rooted at the freshest production tip — no prompt, fully non-blocking. Home branches are the folder-named placeholder branches each worktree rests on (e.g. `codebyplan-web`); `feat/*` branches are deliberately skipped — they integrate via `/cbp-merge-main` with QA, never an auto fast-forward.
56
-
57
- Run `codebyplan session home-ff` and parse the JSON output (`{ result: 'skipped'|'warn'|'fast_forwarded', reason?, warn? }`):
58
-
59
- - **`result === 'skipped'`** (non-home branch or production mismatch): proceed silently.
60
- - **`result === 'warn'`** (fast-forward attempt failed): surface the `warn` field as one line, then proceed silently.
61
- - **`result === 'fast_forwarded'`** (home branch updated): proceed silently.
62
-
63
- Never rebase, reset, force-push, or stash. A non-fast-forwardable home branch is a signal to reconcile manually, not to overwrite.
64
-
65
53
  ### Step 1.5: Infra Drift Check
66
54
 
67
- Surface — never block — when this worktree's source-monorepo `.claude/` infra has fallen behind. Runs after Step 1.4 and may add one line to the Step 6 output (`$PRODUCTION` is the branch resolved in Step 1.4). Consumer-repo package-version freshness is handled by Step 1.6 (the freshness gate), not here:
55
+ Surface — never block — when this worktree's source-monorepo `.claude/` infra has fallen behind. Runs after Step 1 and may add one line to the Step 6 output (`$PRODUCTION` is `branch_config.production` read in Step 1). Consumer-repo package-version freshness is handled by Step 1.6 (the freshness gate), not here:
68
56
 
69
- - **Monorepo (concept A)** — both `packages/codebyplan-package/templates/` and `scripts/infra-drift.mjs` exist. Step 1.4 skips the fetch on a feat branch, so refresh `origin/$PRODUCTION` best-effort first, then run the reporter:
57
+ - **Monorepo (concept A)** — both `packages/codebyplan-package/templates/` and `scripts/infra-drift.mjs` exist. Refresh `origin/$PRODUCTION` best-effort first, then run the reporter:
70
58
 
71
59
  ```bash
72
60
  git fetch origin "$PRODUCTION" 2>/dev/null || true
@@ -136,8 +124,6 @@ Run `codebyplan session freshness-gate --halt-on-update` and parse the JSON outp
136
124
 
137
125
  On this update-and-halt path the session is NOT continued: `update_session_state(activate)` is NOT called, `create_session_log` is NOT called, and `/cbp-todo` is NOT triggered.
138
126
 
139
- **Home branches are intentionally not guarded.** A worktree's folder-named home branch (e.g. `codebyplan-web`) is neither protected/main nor the canonical source, so the freshness gate returns `result !== 'guarded'` there and this gate fires exactly as on a feat branch. That is deliberate — the update lands on whatever branch the worktree is currently resting on (the "update the branch they're on, except main/canonical" decision), not an accidental halt.
140
-
141
127
  Populate the claude-status cache best-effort (pure cache population — never gates session-start):
142
128
 
143
129
  ```bash
@@ -271,7 +257,7 @@ Three-branch gate using `owned_count` and `total_count` from Step 5.8:
271
257
 
272
258
  - **Triggered by**: user invocation, `/clear` recovery
273
259
  - **Resolves**: `npx codebyplan resolve-worktree --json` (worktree id + distress signal; non-tuple-miss distress is non-blocking at session-start)
274
- - **Reads**: `.codebyplan/repo.json`, `.codebyplan/git.json` (`branch_config.production` for the Step 1.4 home-branch fast-forward), MCP `health_check` (Step 0 hard gate — stays MCP unconditionally); local-first reads (with `npx codebyplan sync` + MCP break-glass): `.codebyplan/state/session/current.json` (Step 4 previous log + Step 4.5 handoff probe), `.codebyplan/state/checkpoints/<id>.json` / `tasks/<id>.json` / `rounds/<id>.json` (Step 4.5 freshness probe), `.codebyplan/state/todos.json` (Step 5.7 active-task lookup); MCP `get_checkpoints({ repo_id, status: 'active' })` (Step 5.8 ownership partition — MCP only, no local mirror for active-filter query); `scripts/infra-drift.mjs` + a best-effort `git fetch` (Step 1.5 monorepo drift); `npx codebyplan arch-map drift` + `.codebyplan/architecture.json` presence (Step 1.55 architecture-map drift nudge, non-blocking); `codebyplan session home-ff` (Step 1.4 home-branch fast-forward); `codebyplan session freshness-gate --halt-on-update` (Step 1.6 package-freshness gate); `codebyplan session infra-files --json --task-files <csv>` (Step 5.7 infra-file set math); `npx codebyplan lsp --check` (Step 1.7 LSP binary nudge — reads `.codebyplan/lsp.json`, non-blocking). Reads at Step 3 and later do NOT fire on a Step 0 MCP hard-fail or the Step 1.6 update-and-halt path
260
+ - **Reads**: `.codebyplan/repo.json`, `.codebyplan/git.json` (`branch_config.production` for Step 1.5 infra-drift fetch), MCP `health_check` (Step 0 hard gate — stays MCP unconditionally); local-first reads (with `npx codebyplan sync` + MCP break-glass): `.codebyplan/state/session/current.json` (Step 4 previous log + Step 4.5 handoff probe), `.codebyplan/state/checkpoints/<id>.json` / `tasks/<id>.json` / `rounds/<id>.json` (Step 4.5 freshness probe), `.codebyplan/state/todos.json` (Step 5.7 active-task lookup); MCP `get_checkpoints({ repo_id, status: 'active' })` (Step 5.8 ownership partition — MCP only, no local mirror for active-filter query); `scripts/infra-drift.mjs` + a best-effort `git fetch` (Step 1.5 monorepo drift); `npx codebyplan arch-map drift` + `.codebyplan/architecture.json` presence (Step 1.55 architecture-map drift nudge, non-blocking); `codebyplan session freshness-gate --halt-on-update` (Step 1.6 package-freshness gate); `codebyplan session infra-files --json --task-files <csv>` (Step 5.7 infra-file set math); `npx codebyplan lsp --check` (Step 1.7 LSP binary nudge — reads `.codebyplan/lsp.json`, non-blocking). Reads at Step 3 and later do NOT fire on a Step 0 MCP hard-fail or the Step 1.6 update-and-halt path
275
261
  - **Writes**: `codebyplan session create-log` (Step 5 — CLI write-through; break-glass: MCP `create_session_log`), `codebyplan session update-state --action activate` (Step 3 — CLI write-through to `.codebyplan/state/session/state.json`; break-glass: MCP `update_session_state`) — both SKIPPED on a Step 0 MCP hard-fail and on the Step 1.6 update-and-halt path
276
262
  - **Spawns**: none
277
263
  - **Triggers**: `/cbp-git-commit` (conditional, on user approval at Step 5.7 or the Step 1.6 update path), `handoff.command` (on fresh handoff hit at Step 4.5), `/cbp-todo` (auto fall-through when owned_count >= 1 or total_count === 0; STOPS with no trigger when total_count >= 1 AND owned_count === 0; NOT triggered on a Step 0 hard-fail or the Step 1.6 update-and-halt path)
@@ -170,7 +170,7 @@ Stop.
170
170
 
171
171
  ### Record connection
172
172
 
173
- Record the branch so cleanup (see `cbp-checkpoint-end`, `cbp-git-worktree-remove`) and other
173
+ Record the branch so cleanup (see `cbp-checkpoint-end`, `codebyplan worktree remove CHK-NNN`) and other
174
174
  skills can discover it. Phrase any context payload as prose — the MCP edge rejects raw
175
175
  uppercase database keywords (Cloudflare WAF).
176
176
 
@@ -1,199 +0,0 @@
1
- ---
2
- name: cbp-git-worktree-create
3
- description: Create git worktree with clean command setup and register in CodeByPlan
4
- argument-hint: <branch-name> e.g. codebyplan-app
5
- effort: low
6
- ---
7
-
8
- # Git Worktree Create
9
-
10
- Create a git worktree as a sibling folder and register it in CodeByPlan. The worktree gets its own full copy of `.claude/` files on disk (rules, skills, hooks, agents, context) — git deduplicates at the object level so there's no real cost.
11
-
12
- ## Arguments
13
-
14
- `$ARGUMENTS`: branch name — becomes both the branch name and the folder name.
15
-
16
- Examples:
17
- - `codebyplan-app` → folder `../codebyplan-app/`, branch `codebyplan-app`
18
- - `codebyplan-package` → folder `../codebyplan-package/`, branch `codebyplan-package`
19
-
20
- ## Prerequisites
21
-
22
- - Must run from the **main repo** (the non-worktree checkout)
23
- - Working tree should be clean
24
- - Branch should not already exist as a worktree
25
- - New branches are always rooted at `origin/{production}` (read from `.codebyplan/git.json`), independent of the main repo's currently checked-out branch
26
-
27
- ## Instructions
28
-
29
- ### Step 1: Verify Main Repo
30
-
31
- ```bash
32
- MAIN_REPO="$(git rev-parse --show-toplevel)"
33
- ```
34
-
35
- Verify this is the main repo (not itself a worktree):
36
-
37
- ```bash
38
- git rev-parse --git-dir
39
- ```
40
-
41
- If the result is a file (not `.git` directory), this is already a worktree. Stop:
42
-
43
- ```
44
- ## Error
45
-
46
- Run this command from the main repo, not from a worktree.
47
- Main repo: [path from git-common-dir parent]
48
- ```
49
-
50
- ### Step 2: Validate Arguments
51
-
52
- If `$ARGUMENTS` is empty, ask the user:
53
-
54
- ```
55
- What should the worktree be called? (e.g. codebyplan-app)
56
- ```
57
-
58
- Set:
59
- - `BRANCH_NAME` = `$ARGUMENTS`
60
- - `WORKTREE_PATH` = `$MAIN_REPO/../$BRANCH_NAME`
61
-
62
- ### Step 3: Check for Conflicts
63
-
64
- Check if the worktree already exists:
65
-
66
- ```bash
67
- git worktree list | grep "$BRANCH_NAME"
68
- ```
69
-
70
- If it exists:
71
- ```
72
- ## Worktree Already Exists
73
-
74
- Branch `[name]` is already checked out at [path].
75
-
76
- Use `/cbp-git-worktree-remove [name]` to remove it first.
77
- ```
78
- Stop here.
79
-
80
- Check if the folder already exists:
81
-
82
- ```bash
83
- ls -d "$WORKTREE_PATH" 2>/dev/null
84
- ```
85
-
86
- If it exists, stop and inform the user.
87
-
88
- ### Step 4: Create Branch If Needed
89
-
90
- Resolve the production branch: read `.codebyplan/git.json` and take `branch_config.production` (fall back to `main` if the file or field is absent). Call this `$PRODUCTION`.
91
-
92
- Check if branch exists:
93
-
94
- ```bash
95
- git branch --list "$BRANCH_NAME"
96
- ```
97
-
98
- If branch does NOT exist, create it from the freshest production tip — **not** from the main repo's ambient HEAD (which is often a stale home branch such as `codebyplan-main`):
99
-
100
- ```bash
101
- git fetch origin "$PRODUCTION" 2>/dev/null || true
102
- git branch "$BRANCH_NAME" "origin/$PRODUCTION"
103
- ```
104
-
105
- This guarantees every worktree starts at `origin/{production}` regardless of which branch the main repo currently has checked out.
106
-
107
- ### Step 5: Create Worktree
108
-
109
- ```bash
110
- git worktree add "$WORKTREE_PATH" "$BRANCH_NAME"
111
- ```
112
-
113
- ### Step 6: Set Up MCP Connection
114
-
115
- Copy `.mcp.json` from the main repo to the worktree. The file is committed and contains only the public MCP URL (no secret), so this is a plain `cp` — no path rewriting or key substitution needed:
116
-
117
- ```bash
118
- cp "$MAIN_REPO/.mcp.json" "$WORKTREE_PATH/.mcp.json"
119
- ```
120
-
121
- Expected shape (do NOT rewrite paths — this is remote HTTP):
122
-
123
- ```json
124
- {
125
- "mcpServers": {
126
- "codebyplan": {
127
- "type": "http",
128
- "url": "https://mcp.codebyplan.com/mcp"
129
- }
130
- }
131
- }
132
- ```
133
-
134
- ### Step 7: Set Up Environment
135
-
136
- Copy `.env.local` from the main repo to the worktree (contains the app's environment variables such as Supabase keys and feature flags — MCP auth is OAuth Bearer handled by Claude Code, not a key in this file):
137
-
138
- ```bash
139
- cp "$MAIN_REPO/.env.local" "$WORKTREE_PATH/.env.local"
140
- ```
141
-
142
- Verify `.env.local` is already in `.gitignore` (it should be via `.env.local` pattern). If not, add it.
143
-
144
- Also copy the gitignored E2E credentials source (`.codebyplan/e2e.env`, referenced by `.codebyplan/e2e.json`) so the new worktree can run Playwright auth flows immediately:
145
-
146
- ```bash
147
- mkdir -p "$WORKTREE_PATH/.codebyplan"
148
- if [ -f "$MAIN_REPO/.codebyplan/e2e.env" ]; then
149
- cp "$MAIN_REPO/.codebyplan/e2e.env" "$WORKTREE_PATH/.codebyplan/e2e.env"
150
- fi
151
- ```
152
-
153
- If the main repo has no `.codebyplan/e2e.env` yet, provision it after setup by running `codebyplan ports --path "$WORKTREE_PATH" --provision-e2e` (copies the canonical E2E vars from `apps/web/.env.local`). Pass `--path` BEFORE the boolean flag. `.codebyplan/e2e.env` is gitignored — never commit it.
154
-
155
- ### Step 8: Push Branch
156
-
157
- ```bash
158
- cd "$WORKTREE_PATH" && git push -u origin "$BRANCH_NAME"
159
- ```
160
-
161
- ### Step 9: Register Worktree and Write `.codebyplan/` Config
162
-
163
- Run `codebyplan worktree create "$BRANCH_NAME" --path "$WORKTREE_PATH"` and parse the JSON output (`{ worktree_files_written: boolean, mcp_registered: boolean, worktree_id?, warn? }`):
164
-
165
- - If `warn` is present: surface it as a non-blocking warning.
166
- - Save the returned `worktree_id` for reference (if present).
167
-
168
- The CLI atomically writes the `.codebyplan/` directory with per-concern config stubs and registers the worktree in the CodeByPlan database. The `.codebyplan/device.local.json` file is created by `npx codebyplan setup` on the device (gitignored). The `worktree_id` is never COMMITTED; it may be cached per-device in the gitignored `.codebyplan/worktree.local.json` (branch-keyed, re-derivable via `codebyplan resolve-worktree --cache`), otherwise resolved at runtime from the `(device_id, repo path, branch)` tuple via `npx codebyplan resolve-worktree`.
169
-
170
- No need to mark as `skip-worktree` — the committed files are merge-safe per CHK-108 and CHK-120.
171
-
172
- ### Step 10: Show Result
173
-
174
- ```
175
- ## Worktree Created
176
-
177
- **Branch**: [branch-name]
178
- **Path**: [worktree-path]
179
- **Main repo**: [main-repo-path]
180
- **Base**: origin/[production] ([sha])
181
- **CodeByPlan**: Registered (worktree ID: [id])
182
-
183
- ### Setup
184
- - MCP: connected (remote endpoint, OAuth Bearer)
185
- - `.claude/` files: full copy on disk (rules, skills, hooks, agents, context)
186
-
187
- ### Next Steps
188
- - Open `[worktree-path]` in your editor
189
- - Run `/cbp-session-start` to begin working
190
- - Checkpoints can be assigned to this worktree via `worktree_id`
191
-
192
- ### Existing Worktrees
193
- [output of git worktree list]
194
- ```
195
-
196
- ## Integration
197
-
198
- - **Related**: `/cbp-git-worktree-remove` (cleanup and deregister)
199
- - **CLI**: `codebyplan worktree create <name> --path <abs>` (Step 9 — writes `.codebyplan/` config and registers worktree)
@@ -1,144 +0,0 @@
1
- ---
2
- name: cbp-git-worktree-remove
3
- description: Remove git worktree and deregister from CodeByPlan
4
- argument-hint: <name> e.g. codebyplan-app
5
- effort: low
6
- ---
7
-
8
- # Git Worktree Remove
9
-
10
- Remove a git worktree folder, deregister it from CodeByPlan, and optionally delete its branch.
11
-
12
- ## Arguments
13
-
14
- `$ARGUMENTS`: worktree name or path to remove.
15
-
16
- ## Prerequisites
17
-
18
- - Must run from the **main repo** (not from the worktree being removed)
19
-
20
- ## Instructions
21
-
22
- ### Step 1: Verify Main Repo
23
-
24
- ```bash
25
- MAIN_REPO="$(git rev-parse --show-toplevel)"
26
- ```
27
-
28
- Verify not in a worktree (`.git` should be a directory, not a file).
29
-
30
- ### Step 2: List Worktrees
31
-
32
- ```bash
33
- git worktree list
34
- ```
35
-
36
- If `$ARGUMENTS` is empty, show the list and ask which to remove.
37
-
38
- ### Step 3: Resolve Worktree
39
-
40
- Find matching worktree from the list. Match by:
41
- 1. Exact path
42
- 2. Folder name (e.g. `codebyplan-app`)
43
- 3. Branch name
44
-
45
- If no match found, show available worktrees and stop.
46
-
47
- Set:
48
- - `WORKTREE_PATH` = resolved path
49
- - `BRANCH_NAME` = branch checked out in that worktree
50
-
51
- ### Step 4: Look Up and Deregister Worktree in CodeByPlan
52
-
53
- Run `codebyplan worktree remove "$BRANCH_NAME"` and parse the JSON output (`{ mcp_deregistered: boolean, warn? }`):
54
-
55
- - If `warn` is present: surface it as a non-blocking warning. The worktree was not found in CodeByPlan or deregistration failed, but local removal will proceed.
56
- - If `mcp_deregistered === true`: worktree was successfully deregistered.
57
- - If `mcp_deregistered === false`: worktree was not registered or deregistration failed; continue with local removal.
58
-
59
- ### Step 5: Check for Assigned Checkpoints
60
-
61
- If the worktree was successfully deregistered in Step 4 (`mcp_deregistered === true`), any checkpoints that were assigned to it will become unassigned. This is expected behavior and requires no additional action.
62
-
63
- ### Step 6: Confirm with User
64
-
65
- ```
66
- ## Remove Worktree
67
-
68
- **Path**: [worktree-path]
69
- **Branch**: [branch-name]
70
- **CodeByPlan**: [registered | not registered]
71
-
72
- Remove worktree and delete the branch? (The main repo is not affected)
73
- ```
74
-
75
- Ask:
76
- 1. Remove worktree only (keep branch)
77
- 2. Remove worktree and delete branch
78
- 3. Cancel
79
-
80
- ### Step 7: Remove Git Worktree
81
-
82
- ```bash
83
- git worktree remove "$WORKTREE_PATH"
84
- ```
85
-
86
- If force needed (uncommitted changes):
87
- ```bash
88
- git worktree remove --force "$WORKTREE_PATH"
89
- ```
90
-
91
- Only use `--force` if the user confirms.
92
-
93
- ### Step 8: Delete Branch (if requested)
94
-
95
- **Protected branch check:** Read the protected set from `.codebyplan/git.json`:
96
- ```bash
97
- PRODUCTION=$(jq -r '.branch_config.production // "main"' .codebyplan/git.json)
98
- PROTECTED=$(jq -r '.branch_config.protected[]? // empty' .codebyplan/git.json)
99
- ```
100
- If `$BRANCH_NAME` equals `$PRODUCTION` or appears in `$PROTECTED` — refuse deletion and stop.
101
-
102
- **Checkpoint verification:** Before deleting a feat branch, verify that the associated checkpoint has completed via `/cbp-checkpoint-end`. If the checkpoint is still active, warn the user that unshipped work may be lost.
103
-
104
- ```bash
105
- git branch -d "$BRANCH_NAME" && git push origin --delete "$BRANCH_NAME"
106
- ```
107
-
108
- Use `-d` (not `-D`) to prevent deleting unmerged work. If it fails because the branch is not fully merged, inform the user and ask if they want to force delete with `-D`.
109
-
110
- After the git branch delete succeeds, run a conditional Supabase preview-branch teardown for `$BRANCH_NAME`:
111
-
112
- > Lifecycle contract: see [[supabase-branch-lifecycle]].
113
-
114
- - Resolve the parent project ref and apply the lifecycle guard in one deterministic call:
115
-
116
- ```bash
117
- codebyplan supabase teardown-preview "$BRANCH_NAME"
118
- ```
119
-
120
- Parse its JSON `{ status, parent_ref, project_ref, reason }`. The command never deletes anything — it reads the parent ref from `.codebyplan/shipment.json` (`.shipment.surfaces.supabase.project_ref`) and applies the protected / production / parent-ref guard from [[supabase-branch-lifecycle]].
121
- - If `status === "rejected"`: STOP the teardown and surface `reason` — never delete a production / protected / integration branch or one whose preview ref equals the parent.
122
- - Otherwise (`allowed` or `not_found`), use `parent_ref` for the live existence check — `mcp__supabase__list_branches` with `project_id: <parent_ref>`, then scan for an entry whose `name` exactly equals `$BRANCH_NAME`:
123
- - If found: call `mcp__supabase__delete_branch` with its `branch_id`. Report "Supabase preview branch deleted: `$BRANCH_NAME`".
124
- - If not found: no-op silently — the GitHub integration may have already removed it on PR close; not-found is success, NOT an error.
125
- - If the `list_branches` call itself fails (network, auth, or a non-success response — distinct from a successful lookup that returns no match): emit a non-blocking warning that the Supabase preview branch for `$BRANCH_NAME` may still exist and should be verified in the dashboard. Do not treat an API failure as a not-found success.
126
- - Never delete the parent project (`parent_ref` from `codebyplan supabase teardown-preview`) itself or any persistent/production branch — the `teardown-preview` guard enforces this.
127
-
128
- ### Step 9: Show Result
129
-
130
- ```
131
- ## Worktree Removed
132
-
133
- **Removed**: [worktree-path]
134
- **Branch**: [deleted | kept]
135
- **CodeByPlan**: [deregistered | was not registered]
136
-
137
- ### Remaining Worktrees
138
- [output of git worktree list]
139
- ```
140
-
141
- ## Integration
142
-
143
- - **Related**: `/cbp-git-worktree-create` (create and register)
144
- - **CLI**: `codebyplan worktree remove <name>` (Step 4 — deregister from CodeByPlan)