codebyplan 1.13.49 → 1.13.51

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.
Files changed (34) hide show
  1. package/dist/cli.js +447 -14
  2. package/package.json +1 -1
  3. package/templates/agents/cbp-round-executor.md +1 -6
  4. package/templates/agents/cbp-task-planner.md +2 -2
  5. package/templates/hooks/cbp-skill-context-guard.sh +52 -0
  6. package/templates/hooks/cbp-test-hooks.sh +144 -0
  7. package/templates/hooks/hooks.json +9 -0
  8. package/templates/rules/model-invocation-convention.md +40 -0
  9. package/templates/rules/parallel-waves.md +1 -1
  10. package/templates/rules/supabase-branch-lifecycle.md +2 -3
  11. package/templates/rules/task-routing-recommendation.md +1 -1
  12. package/templates/settings.project.base.json +2 -3
  13. package/templates/skills/cbp-build-cc-mode/SKILL.md +1 -1
  14. package/templates/skills/cbp-build-cc-settings/reference/cbp-permission-policy.md +42 -0
  15. package/templates/skills/cbp-checkpoint-create/SKILL.md +1 -0
  16. package/templates/skills/cbp-checkpoint-start/SKILL.md +1 -0
  17. package/templates/skills/cbp-clear-continue/SKILL.md +86 -0
  18. package/templates/skills/cbp-clear-prep/SKILL.md +121 -0
  19. package/templates/skills/cbp-round-start/SKILL.md +1 -1
  20. package/templates/skills/cbp-session-end/SKILL.md +2 -18
  21. package/templates/skills/cbp-session-start/SKILL.md +4 -18
  22. package/templates/skills/cbp-supabase-migrate/SKILL.md +1 -1
  23. package/templates/skills/cbp-task-check/SKILL.md +12 -5
  24. package/templates/skills/cbp-task-complete/SKILL.md +9 -11
  25. package/templates/skills/cbp-task-complete/reference/checkpoint-done-branching.md +14 -21
  26. package/templates/skills/cbp-task-complete/reference/next-step-heuristic.md +4 -6
  27. package/templates/skills/cbp-task-testing/SKILL.md +9 -14
  28. package/templates/skills/cbp-frontend-a11y/SKILL.md +0 -108
  29. package/templates/skills/cbp-frontend-a11y/reference/aria-roles-states.md +0 -130
  30. package/templates/skills/cbp-frontend-a11y/reference/contrast-visual.md +0 -122
  31. package/templates/skills/cbp-frontend-a11y/reference/keyboard-patterns.md +0 -154
  32. package/templates/skills/cbp-frontend-a11y/reference/semantic-html.md +0 -111
  33. package/templates/skills/cbp-git-worktree-create/SKILL.md +0 -199
  34. package/templates/skills/cbp-git-worktree-remove/SKILL.md +0 -144
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.49";
42
+ VERSION = "1.13.51";
43
43
  PACKAGE_NAME = "codebyplan";
44
44
  }
45
45
  });
@@ -639,6 +639,7 @@ var init_gitignore_block = __esm({
639
639
  ".codebyplan/statusline.local.json",
640
640
  ".codebyplan/worktree.local.json",
641
641
  ".codebyplan/state/",
642
+ ".codebyplan/clear/",
642
643
  ".codebyplan/todo/",
643
644
  ".codebyplan/claude-status.local.json",
644
645
  ".codebyplan.local.json"
@@ -14279,8 +14280,8 @@ var require_RealtimeChannel = __commonJS({
14279
14280
  }
14280
14281
  /** @internal */
14281
14282
  _notThisChannelEvent(event, ref) {
14282
- const { close, error, leave, join: join46 } = constants_1.CHANNEL_EVENTS;
14283
- const events = [close, error, leave, join46];
14283
+ const { close, error, leave, join: join48 } = constants_1.CHANNEL_EVENTS;
14284
+ const events = [close, error, leave, join48];
14284
14285
  return ref && events.includes(event) && ref !== this.joinPush.ref;
14285
14286
  }
14286
14287
  /** @internal */
@@ -37931,8 +37932,242 @@ var init_validate_waves2 = __esm({
37931
37932
  }
37932
37933
  });
37933
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
+
37934
38169
  // src/cli/worktree/create.ts
37935
- import { join as join45 } from "node:path";
38170
+ import { join as join47 } from "node:path";
37936
38171
  async function defaultGetRepoIdentity(cwd) {
37937
38172
  const found = await findCodebyplanConfig(cwd);
37938
38173
  const contents = found?.contents ?? null;
@@ -37944,7 +38179,7 @@ async function defaultGetRepoIdentity(cwd) {
37944
38179
  project_id: typeof contents?.["project_id"] === "string" ? contents["project_id"] : void 0
37945
38180
  };
37946
38181
  }
37947
- async function defaultRegisterWorktree(repoId, name, path16) {
38182
+ async function defaultRegisterWorktree2(repoId, name, path16) {
37948
38183
  const res = await apiPost("/worktrees", {
37949
38184
  repo_id: repoId,
37950
38185
  name,
@@ -37976,7 +38211,7 @@ async function runWorktreeCreate(args, deps = {}) {
37976
38211
  const cwd = deps.cwd ?? process.cwd();
37977
38212
  const getRepoIdentity = deps.getRepoIdentity ?? defaultGetRepoIdentity;
37978
38213
  const getDeviceId = deps.getDeviceId ?? getOrCreateDeviceId;
37979
- const registerWorktree = deps.registerWorktree ?? defaultRegisterWorktree;
38214
+ const registerWorktree = deps.registerWorktree ?? defaultRegisterWorktree2;
37980
38215
  const identity = await getRepoIdentity(cwd);
37981
38216
  const repoId = identity?.repo_id ?? null;
37982
38217
  if (!identity || !repoId) {
@@ -37987,7 +38222,7 @@ async function runWorktreeCreate(args, deps = {}) {
37987
38222
  );
37988
38223
  return 1;
37989
38224
  }
37990
- const worktreePath = explicitPath ?? join45(cwd, "..", name);
38225
+ const worktreePath = explicitPath ?? join47(cwd, "..", name);
37991
38226
  const deviceId = await getDeviceId(cwd);
37992
38227
  let filesWritten = false;
37993
38228
  try {
@@ -38043,7 +38278,9 @@ var init_create = __esm({
38043
38278
  });
38044
38279
 
38045
38280
  // src/cli/worktree/remove.ts
38046
- 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) {
38047
38284
  const found = await findCodebyplanConfig(cwd);
38048
38285
  return found?.contents.repo_id ?? null;
38049
38286
  }
@@ -38063,6 +38300,192 @@ async function defaultFindWorktreeId(repoId, name) {
38063
38300
  async function defaultDeregisterWorktree(worktreeId) {
38064
38301
  await mcpCall("delete_worktree", { worktree_id: worktreeId });
38065
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
+ }
38066
38489
  async function runWorktreeRemove(args, deps = {}) {
38067
38490
  const name = args.find((a) => !a.startsWith("--"));
38068
38491
  if (!name) {
@@ -38073,8 +38496,12 @@ async function runWorktreeRemove(args, deps = {}) {
38073
38496
  );
38074
38497
  return 1;
38075
38498
  }
38499
+ const chkNumber = parseChkNumber(name);
38500
+ if (chkNumber !== null) {
38501
+ return runWorktreeRemoveChk(chkNumber, deps);
38502
+ }
38076
38503
  const cwd = deps.cwd ?? process.cwd();
38077
- const getRepoId = deps.getRepoId ?? defaultGetRepoId;
38504
+ const getRepoId = deps.getRepoId ?? defaultGetRepoId2;
38078
38505
  const findWorktreeId = deps.findWorktreeId ?? defaultFindWorktreeId;
38079
38506
  const deregisterWorktree = deps.deregisterWorktree ?? defaultDeregisterWorktree;
38080
38507
  const repoId = await getRepoId(cwd);
@@ -38113,9 +38540,11 @@ async function runWorktreeRemove(args, deps = {}) {
38113
38540
  var init_remove = __esm({
38114
38541
  "src/cli/worktree/remove.ts"() {
38115
38542
  "use strict";
38543
+ init_path();
38116
38544
  init_api();
38117
38545
  init_mcp_client();
38118
38546
  init_flags();
38547
+ init_supabase();
38119
38548
  }
38120
38549
  });
38121
38550
 
@@ -38126,6 +38555,9 @@ __export(worktree_exports, {
38126
38555
  });
38127
38556
  async function runWorktreeCommand(args) {
38128
38557
  const subcommand = args[0];
38558
+ if (subcommand === "add") {
38559
+ return runWorktreeAdd(args.slice(1));
38560
+ }
38129
38561
  if (subcommand === "create") {
38130
38562
  return runWorktreeCreate(args.slice(1));
38131
38563
  }
@@ -38139,7 +38571,7 @@ async function runWorktreeCommand(args) {
38139
38571
  if (subcommand) {
38140
38572
  process.stderr.write(
38141
38573
  `Unknown subcommand: codebyplan worktree ${subcommand}
38142
- Available: create, remove
38574
+ Available: add, create, remove
38143
38575
  `
38144
38576
  );
38145
38577
  return 1;
@@ -38149,12 +38581,13 @@ Available: create, remove
38149
38581
  }
38150
38582
  function printWorktreeHelp() {
38151
38583
  process.stdout.write(
38152
- "\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"
38153
38585
  );
38154
38586
  }
38155
38587
  var init_worktree2 = __esm({
38156
38588
  "src/cli/worktree.ts"() {
38157
38589
  "use strict";
38590
+ init_add();
38158
38591
  init_create();
38159
38592
  init_remove();
38160
38593
  }
@@ -38274,7 +38707,7 @@ var init_e2e = __esm({
38274
38707
  });
38275
38708
 
38276
38709
  // src/cli/e2e/verify-round.ts
38277
- import { readFile as readFile30 } from "node:fs/promises";
38710
+ import { readFile as readFile31 } from "node:fs/promises";
38278
38711
  import { resolve as resolve9 } from "node:path";
38279
38712
  async function defaultFetchRounds(taskId) {
38280
38713
  return mcpCall("get_rounds", { task_id: taskId });
@@ -38282,7 +38715,7 @@ async function defaultFetchRounds(taskId) {
38282
38715
  async function defaultReadE2eConfig(cwd) {
38283
38716
  try {
38284
38717
  const p = resolve9(cwd, ".codebyplan", "e2e.json");
38285
- const raw = await readFile30(p, "utf-8");
38718
+ const raw = await readFile31(p, "utf-8");
38286
38719
  return JSON.parse(raw);
38287
38720
  } catch {
38288
38721
  return null;
@@ -39062,7 +39495,7 @@ void (async () => {
39062
39495
  codebyplan claude Claude asset management (install/update/uninstall/generate/migrate-memory)
39063
39496
  codebyplan supabase Preview-branch helpers (resolve-preview/teardown-preview/new-migration/preview-check)
39064
39497
  codebyplan session Session helpers (home-ff/freshness-gate/infra-files)
39065
- codebyplan worktree Worktree management (create/remove)
39498
+ codebyplan worktree Worktree management (add/create/remove)
39066
39499
  codebyplan e2e E2E deterministic gates (verify-round)
39067
39500
  codebyplan statusline Show or set the statusline renderer (bash/node/python)
39068
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.49",
3
+ "version": "1.13.51",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -239,18 +239,13 @@ When the executor received a `wave` input with a non-empty `wave.skill_preloads[
239
239
  For each entry in `wave.skill_preloads[]`, invoke the named skill via the Skill tool BEFORE Step 3 (Execute). Invoke in order:
240
240
 
241
241
  1. `cbp-frontend-design` — if present, invoke FIRST (aesthetic direction before code)
242
- 2. `cbp-frontend-a11y` if present, invoke AFTER `cbp-frontend-design` (accessibility obligations)
243
- 3. Any other skill preload — invoke in list order
242
+ 2. Any other skill preload invoke in list order
244
243
 
245
244
  Record completion:
246
245
  ```yaml
247
246
  round.context.frontend_design_loaded: true # if cbp-frontend-design was preloaded
248
- round.context.frontend_a11y_loaded: true # if cbp-frontend-a11y was preloaded
249
- round.context.frontend_a11y_checklist: [items from cbp-frontend-a11y/SKILL.md Phase 6 output] # only when cbp-frontend-a11y was preloaded for this wave
250
247
  ```
251
248
 
252
- When cbp-frontend-a11y is preloaded, capture its Phase 6 per-component checklist output verbatim into `round.context.frontend_a11y_checklist`. Step 3 reads this for accessibility enforcement during code emission.
253
-
254
249
  If `wave` is absent or `wave.skill_preloads[]` is empty, skip this step — Step 2.7 handles the non-wave UI pre-read path.
255
250
 
256
251
  **Why step 2.6 and 2.7 coexist**: Step 2.7 fires for non-wave rounds when the executor detects UI files directly. Step 2.6 fires for wave rounds where the planner already determined the preloads. They cover the same skill but via different trigger paths; the round.context recording is identical so downstream steps behave uniformly.
@@ -533,7 +533,7 @@ After Phase 5 (solution design) and before Phase 6 (context summary), decompose
533
533
  1. **Identify natural cut points**: look for cross-app boundaries (files in `apps/web/` vs `apps/backend/` vs `apps/desktop/`), packages with no shared state, or dependency ordering (DB migration must precede app code using the new schema).
534
534
  2. **Check disjoint-files invariant**: no file may appear in two waves. If a shared file is needed by two waves, assign it to the earlier wave and make the later wave `depends_on` the earlier.
535
535
  3. **Check DAG invariant**: `depends_on[]` must be acyclic. Any cycle is a plan error — resolve by merging the cyclic waves.
536
- 4. **Populate `skill_preloads[]`**: for each wave whose `files[]` contains UI-bearing paths (`*.tsx`, `*.jsx`, `*.scss`, etc.), add `"frontend-design"` and `"frontend-a11y"` to `skill_preloads[]` (in that order).
536
+ 4. **Populate `skill_preloads[]`**: for each wave whose `files[]` contains UI-bearing paths (`*.tsx`, `*.jsx`, `*.scss`, etc.), add `"frontend-design"` to `skill_preloads[]`.
537
537
  5. **Single-wave default**: if no independence is found, produce ONE wave covering all files. Parallel waves add orchestration overhead — only decompose when the benefit is clear.
538
538
  6. **15-file cap**: after decomposition (including the single-wave default), count files in each wave. If any wave would exceed 15 files, auto-split it using the proximity-split algorithm in priority order: (a) **shared directory subtree** — split at the deepest common ancestor that produces two groups each ≥3 files; (b) **shared module** — split at the next directory level below the common ancestor; (c) **arbitrary boundary** — split at the 15-file boundary and add a one-line `note` on the continuation wave explaining the boundary. Split siblings are **independent**: do NOT add `depends_on` between them unless a real shared-file or data dependency requires ordering. **Tail rule**: choose boundaries so every resulting wave holds 3–15 files. A split must never leave a wave with <3 files; rebalance the boundary rather than absorbing a tail into a sibling in a way that pushes it above 15. The 3–15 range is a hard invariant — there is no exception above 15. **Apply the cap iteratively**: after a split, re-check each resulting wave and split again any that still exceeds 15 — a 40-file single-concern plan therefore yields ≥3 waves. When no natural boundary yields groups each ≥3 files, take the smallest ≥3-file prefix as one wave and apply the same procedure to the remainder. The single-wave default is itself subject to this cap. See `rules/parallel-waves.md` for the full algorithm and invariants.
539
539
 
@@ -559,7 +559,7 @@ printf '%s' "$PLAN_JSON" | codebyplan validate-waves --json
559
559
 
560
560
  (`$PLAN_JSON` is the `{ "waves": [...] }` structure; pass a file path as the first argument instead of stdin if preferred.) Exit 0 = invariants I–III satisfied. Exit non-zero = one or more violations — the `--json` `violations[]` array names the failing invariant (`I`/`II`/`III`) and offending wave/file; fix the decomposition and re-run before emitting the plan. The validator does NOT check invariant IV (UI skill preloads) — that remains a manual step:
561
561
 
562
- - [ ] UI-bearing waves have `frontend-design` + `frontend-a11y` in `skill_preloads[]` (invariant IV — not covered by `validate-waves`)
562
+ - [ ] UI-bearing waves have `frontend-design` in `skill_preloads[]` (invariant IV — not covered by `validate-waves`)
563
563
 
564
564
  ### Phase 6: Build Context Summary
565
565
 
@@ -0,0 +1,52 @@
1
+ #!/bin/bash
2
+ # @scope: org-shared
3
+ # Hook: PreToolUse (Skill)
4
+ # Purpose: Deny heavy close-out skills when context window > CBP_CONTEXT_WARN_TOKENS (default 200000).
5
+ # Reads transcript_path from stdin, sums the latest assistant message.usage — same logic
6
+ # as cbp-context-window-notify.sh. If total exceeds threshold AND the skill is in the
7
+ # heavy close-out allowlist, emits hookSpecificOutput.permissionDecision=deny directing
8
+ # Claude to run /cbp-clear-prep. Always exits 0 — fail-open.
9
+
10
+ set -euo pipefail
11
+
12
+ INPUT=$(cat)
13
+ SKILL_NAME=$(echo "$INPUT" | jq -r '.tool_input.skill // .tool_input.skill_name // ""' 2>/dev/null) || SKILL_NAME=""
14
+ TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // ""' 2>/dev/null) || TRANSCRIPT=""
15
+
16
+ # Fast-path: no transcript → pass through
17
+ [ -z "$TRANSCRIPT" ] && exit 0
18
+ [ ! -f "$TRANSCRIPT" ] && exit 0
19
+
20
+ THRESHOLD="${CBP_CONTEXT_WARN_TOKENS:-200000}"
21
+
22
+ # Heavy close-out allowlist (cbp-clear-prep + cbp-clear-continue deliberately excluded so
23
+ # they always run even when context > threshold).
24
+ HEAVY_SKILLS="cbp-round-execute cbp-task-testing cbp-standalone-task-testing cbp-checkpoint-check cbp-checkpoint-end"
25
+
26
+ # Cheap allowlist check before summing tokens
27
+ IS_HEAVY=false
28
+ for heavy in $HEAVY_SKILLS; do
29
+ if [ "$SKILL_NAME" = "$heavy" ]; then
30
+ IS_HEAVY=true
31
+ break
32
+ fi
33
+ done
34
+ [ "$IS_HEAVY" = "false" ] && exit 0
35
+
36
+ # Token sum — same logic as cbp-context-window-notify.sh
37
+ TOTAL=$(tail -n 400 "$TRANSCRIPT" \
38
+ | jq -rR 'fromjson? | select(.message.usage != null)
39
+ | (.message.usage
40
+ | ((.input_tokens // 0) + (.cache_creation_input_tokens // 0) + (.cache_read_input_tokens // 0)))' \
41
+ 2>/dev/null | tail -1) || TOTAL=0
42
+ TOTAL="${TOTAL:-0}"
43
+
44
+ if [ "$TOTAL" -ge "$THRESHOLD" ] 2>/dev/null; then
45
+ jq -n \
46
+ --argjson tokens "$TOTAL" \
47
+ --argjson threshold "$THRESHOLD" \
48
+ --arg skill "$SKILL_NAME" \
49
+ '{hookSpecificOutput:{permissionDecision:"deny",permissionDecisionReason:("Context window at \($tokens) tokens (threshold \($threshold)) is too large to safely run /\($skill). Run /cbp-clear-prep now to capture a handoff, then /clear, then /cbp-clear-continue to resume.")}}'
50
+ fi
51
+
52
+ exit 0