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.
- package/dist/cli.js +447 -14
- package/package.json +1 -1
- package/templates/agents/cbp-round-executor.md +1 -6
- package/templates/agents/cbp-task-planner.md +2 -2
- package/templates/hooks/cbp-skill-context-guard.sh +52 -0
- package/templates/hooks/cbp-test-hooks.sh +144 -0
- package/templates/hooks/hooks.json +9 -0
- package/templates/rules/model-invocation-convention.md +40 -0
- package/templates/rules/parallel-waves.md +1 -1
- package/templates/rules/supabase-branch-lifecycle.md +2 -3
- package/templates/rules/task-routing-recommendation.md +1 -1
- package/templates/settings.project.base.json +2 -3
- package/templates/skills/cbp-build-cc-mode/SKILL.md +1 -1
- package/templates/skills/cbp-build-cc-settings/reference/cbp-permission-policy.md +42 -0
- package/templates/skills/cbp-checkpoint-create/SKILL.md +1 -0
- package/templates/skills/cbp-checkpoint-start/SKILL.md +1 -0
- package/templates/skills/cbp-clear-continue/SKILL.md +86 -0
- package/templates/skills/cbp-clear-prep/SKILL.md +121 -0
- package/templates/skills/cbp-round-start/SKILL.md +1 -1
- package/templates/skills/cbp-session-end/SKILL.md +2 -18
- package/templates/skills/cbp-session-start/SKILL.md +4 -18
- package/templates/skills/cbp-supabase-migrate/SKILL.md +1 -1
- package/templates/skills/cbp-task-check/SKILL.md +12 -5
- package/templates/skills/cbp-task-complete/SKILL.md +9 -11
- package/templates/skills/cbp-task-complete/reference/checkpoint-done-branching.md +14 -21
- package/templates/skills/cbp-task-complete/reference/next-step-heuristic.md +4 -6
- package/templates/skills/cbp-task-testing/SKILL.md +9 -14
- package/templates/skills/cbp-frontend-a11y/SKILL.md +0 -108
- package/templates/skills/cbp-frontend-a11y/reference/aria-roles-states.md +0 -130
- package/templates/skills/cbp-frontend-a11y/reference/contrast-visual.md +0 -122
- package/templates/skills/cbp-frontend-a11y/reference/keyboard-patterns.md +0 -154
- package/templates/skills/cbp-frontend-a11y/reference/semantic-html.md +0 -111
- package/templates/skills/cbp-git-worktree-create/SKILL.md +0 -199
- 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.
|
|
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:
|
|
14283
|
-
const events = [close, error, leave,
|
|
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
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
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
|
@@ -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.
|
|
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"`
|
|
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`
|
|
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
|