codebyplan 1.13.66 → 1.13.68
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/claude/generate.d.ts +1 -1
- package/dist/cli/claude/generate.d.ts.map +1 -1
- package/dist/cli/claude/readme.d.ts.map +1 -1
- package/dist/cli/worktree/remove.d.ts +40 -8
- package/dist/cli/worktree/remove.d.ts.map +1 -1
- package/dist/cli.js +177 -67
- package/dist/lib/tech-stack-render.d.ts +30 -0
- package/dist/lib/tech-stack-render.d.ts.map +1 -0
- package/package.json +1 -1
- package/templates/hooks/cbp-test-hooks.sh +0 -81
- package/templates/rules/cbp-operating-gotchas.md +8 -10
- package/templates/rules/todo-backend.md +4 -2
- package/templates/skills/cbp-build-cc-settings/reference/cbp-permission-policy.md +3 -3
- package/templates/skills/cbp-checkpoint-end/SKILL.md +11 -5
- package/templates/skills/cbp-finalize/SKILL.md +2 -2
- package/templates/skills/cbp-standalone-task-complete/SKILL.md +40 -0
|
@@ -16,7 +16,7 @@ export interface GenerateOpts {
|
|
|
16
16
|
projectDir?: string;
|
|
17
17
|
/** Print the would-be content to stdout, write nothing. */
|
|
18
18
|
dryRun?: boolean;
|
|
19
|
-
/** Exit non-zero when AGENTS.md is missing or drifted. Read-only — writes nothing. */
|
|
19
|
+
/** Exit non-zero when AGENTS.md or structure.md is missing or drifted. Read-only — writes nothing. */
|
|
20
20
|
check?: boolean;
|
|
21
21
|
}
|
|
22
22
|
export declare function runGenerate(opts: GenerateOpts | Record<string, unknown>): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/cli/claude/generate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAqBH,MAAM,WAAW,YAAY;IAC3B,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../../src/cli/claude/generate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAqBH,MAAM,WAAW,YAAY;IAC3B,gDAAgD;IAChD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,sGAAsG;IACtG,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAyDD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3C,OAAO,CAAC,IAAI,CAAC,CAiOf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readme.d.ts","sourceRoot":"","sources":["../../../src/cli/claude/readme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA4BH,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;
|
|
1
|
+
{"version":3,"file":"readme.d.ts","sourceRoot":"","sources":["../../../src/cli/claude/readme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AA4BH,MAAM,WAAW,UAAU;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA4ID,wBAAsB,SAAS,CAC7B,IAAI,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACzC,OAAO,CAAC,IAAI,CAAC,CAkLf;AAmCD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAoBpE"}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI entry point for `codebyplan worktree remove <name|CHK-NNN>`.
|
|
2
|
+
* CLI entry point for `codebyplan worktree remove <name|CHK-NNN|abs-path>`.
|
|
3
3
|
*
|
|
4
4
|
* Usage: codebyplan worktree remove <name> (legacy name-based path)
|
|
5
5
|
* codebyplan worktree remove <CHK-NNN> (checkpoint path — git rm + Supabase teardown)
|
|
6
6
|
* codebyplan worktree remove <bare-number> (same as CHK-NNN)
|
|
7
|
+
* codebyplan worktree remove <abs-path> (direct path — same logic as CHK-NNN without number computation)
|
|
7
8
|
*
|
|
8
9
|
* Legacy path: deregistration retired in CHK-225 TASK-3; returns { mcp_deregistered: false } with no MCP call.
|
|
9
|
-
* CHK-NNN path: performs git worktree remove and Supabase preview-branch teardown; deregistration removed.
|
|
10
|
+
* CHK-NNN / abs-path: performs git worktree remove and Supabase preview-branch teardown; deregistration removed.
|
|
10
11
|
*
|
|
11
12
|
* Output JSON:
|
|
12
13
|
* Legacy: { mcp_deregistered: boolean, warn? }
|
|
13
|
-
* CHK-NNN: { mcp_deregistered: boolean, git_removed: boolean, supabase_torn_down: boolean,
|
|
14
|
+
* CHK-NNN / abs-path: { mcp_deregistered: boolean, git_removed: boolean, supabase_torn_down: boolean,
|
|
15
|
+
* remote_branch_deleted?: boolean, local_branch_deleted?: boolean, warn? }
|
|
14
16
|
*
|
|
15
|
-
* Exit: 0 on success or non-fatal failure; 1 on usage error.
|
|
17
|
+
* Exit: 0 on success or non-fatal failure; 1 on usage error; 2 when cwd is inside the target worktree.
|
|
16
18
|
*/
|
|
17
19
|
export interface GitRunResult {
|
|
18
20
|
status: number | null;
|
|
@@ -40,21 +42,51 @@ export interface RemoveWorktreeDeps {
|
|
|
40
42
|
* Idempotent: not-found is success ({ deleted: true }).
|
|
41
43
|
*/
|
|
42
44
|
deleteSupabaseBranch?: (branchName: string, cwd: string) => Promise<SupabaseTeardownResult>;
|
|
45
|
+
/**
|
|
46
|
+
* Guard: returns true when cwd is the worktree being removed or a child of it.
|
|
47
|
+
* When true, removal is aborted with exit code 2.
|
|
48
|
+
* Default: cwd === worktreePath || cwd.startsWith(worktreePath + sep)
|
|
49
|
+
*/
|
|
50
|
+
isInsideWorktreePath?: (worktreePath: string, cwd: string) => boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Verify the remote branch exists (ls-remote --exit-code) and delete it when found.
|
|
53
|
+
* Non-fatal: failures emit warnings and never change the exit code.
|
|
54
|
+
* Returns { remote_branch_deleted: boolean, warn?: string }.
|
|
55
|
+
*/
|
|
56
|
+
verifyAndDeleteRemoteBranch?: (branchName: string, cwd: string) => Promise<{
|
|
57
|
+
remote_branch_deleted: boolean;
|
|
58
|
+
warn?: string;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Delete the local tracking branch after the worktree has been removed.
|
|
62
|
+
* Attempts `git branch -d` first; escalates to `git branch -D` only when the
|
|
63
|
+
* remote is confirmed gone (remote_is_gone === true).
|
|
64
|
+
* Non-fatal: failures emit warnings and never change the exit code.
|
|
65
|
+
* Returns { local_branch_deleted: boolean, warn?: string }.
|
|
66
|
+
*/
|
|
67
|
+
deleteFeatBranch?: (branchName: string, cwd: string, remote_is_gone: boolean) => Promise<{
|
|
68
|
+
local_branch_deleted: boolean;
|
|
69
|
+
warn?: string;
|
|
70
|
+
}>;
|
|
43
71
|
}
|
|
44
72
|
export interface RemoveWorktreeResult {
|
|
45
73
|
mcp_deregistered: boolean;
|
|
46
|
-
/** CHK-NNN path
|
|
74
|
+
/** CHK-NNN / abs-path: whether git worktree remove succeeded. */
|
|
47
75
|
git_removed?: boolean;
|
|
48
|
-
/** CHK-NNN path
|
|
76
|
+
/** CHK-NNN / abs-path: whether the Supabase preview branch was deleted. */
|
|
49
77
|
supabase_torn_down?: boolean;
|
|
78
|
+
/** CHK-NNN / abs-path: whether the remote git branch was deleted. */
|
|
79
|
+
remote_branch_deleted?: boolean;
|
|
80
|
+
/** CHK-NNN / abs-path: whether the local git branch was deleted. */
|
|
81
|
+
local_branch_deleted?: boolean;
|
|
50
82
|
warn?: string;
|
|
51
83
|
}
|
|
52
84
|
/**
|
|
53
85
|
* Run `codebyplan worktree remove`.
|
|
54
86
|
*
|
|
55
|
-
* @param args Remaining args after "remove" was stripped — e.g. ["my-branch"]
|
|
87
|
+
* @param args Remaining args after "remove" was stripped — e.g. ["my-branch"], ["CHK-207"], or ["/abs/path"]
|
|
56
88
|
* @param deps Injectable deps for testing.
|
|
57
|
-
* @returns Exit code: 0 success / non-fatal, 1 usage error.
|
|
89
|
+
* @returns Exit code: 0 success / non-fatal, 1 usage error, 2 cwd-inside guard.
|
|
58
90
|
*/
|
|
59
91
|
export declare function runWorktreeRemove(args: string[], deps?: RemoveWorktreeDeps): Promise<number>;
|
|
60
92
|
//# sourceMappingURL=remove.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remove.d.ts","sourceRoot":"","sources":["../../../src/cli/worktree/remove.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"remove.d.ts","sourceRoot":"","sources":["../../../src/cli/worktree/remove.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAWH,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AAED,MAAM,WAAW,sBAAsB;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IAIb,gEAAgE;IAChE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,KAAK,YAAY,CAAC;IACvD;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAChE;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,CACrB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,KACR,OAAO,CAAC,sBAAsB,CAAC,CAAC;IACrC;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACtE;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,CAC5B,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,KACR,OAAO,CAAC;QAAE,qBAAqB,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChE;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,CACjB,UAAU,EAAE,MAAM,EAClB,GAAG,EAAE,MAAM,EACX,cAAc,EAAE,OAAO,KACpB,OAAO,CAAC;QAAE,oBAAoB,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChE;AAoOD,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,2EAA2E;IAC3E,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,qEAAqE;IACrE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,oEAAoE;IACpE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AA4LD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,kBAAuB,GAC5B,OAAO,CAAC,MAAM,CAAC,CAgCjB"}
|
package/dist/cli.js
CHANGED
|
@@ -48,7 +48,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
48
48
|
var init_version = __esm({
|
|
49
49
|
"src/lib/version.ts"() {
|
|
50
50
|
"use strict";
|
|
51
|
-
VERSION = "1.13.
|
|
51
|
+
VERSION = "1.13.68";
|
|
52
52
|
PACKAGE_NAME = "codebyplan";
|
|
53
53
|
}
|
|
54
54
|
});
|
|
@@ -39272,6 +39272,7 @@ var init_create = __esm({
|
|
|
39272
39272
|
});
|
|
39273
39273
|
|
|
39274
39274
|
// src/cli/worktree/remove.ts
|
|
39275
|
+
import { isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
|
|
39275
39276
|
import { spawnSync as spawnSync21 } from "node:child_process";
|
|
39276
39277
|
function defaultGitRun2(args, cwd) {
|
|
39277
39278
|
const result = spawnSync21("git", args, {
|
|
@@ -39387,6 +39388,51 @@ async function defaultDeleteSupabaseBranch(branchName, cwd) {
|
|
|
39387
39388
|
};
|
|
39388
39389
|
}
|
|
39389
39390
|
}
|
|
39391
|
+
function defaultIsInsideWorktreePath(worktreePath, cwd) {
|
|
39392
|
+
return cwd === worktreePath || cwd.startsWith(worktreePath + sep3);
|
|
39393
|
+
}
|
|
39394
|
+
function defaultVerifyAndDeleteRemoteBranch(branchName, cwd) {
|
|
39395
|
+
const gitRun = defaultGitRun2;
|
|
39396
|
+
const lsResult = gitRun(
|
|
39397
|
+
["ls-remote", "--exit-code", "origin", branchName],
|
|
39398
|
+
cwd
|
|
39399
|
+
);
|
|
39400
|
+
if (lsResult.status !== 0) {
|
|
39401
|
+
return Promise.resolve({ remote_branch_deleted: false });
|
|
39402
|
+
}
|
|
39403
|
+
const pushResult = gitRun(["push", "origin", "--delete", branchName], cwd);
|
|
39404
|
+
if (pushResult.status !== 0) {
|
|
39405
|
+
const errMsg = pushResult.stderr.trim() || pushResult.error?.message || "unknown error";
|
|
39406
|
+
return Promise.resolve({
|
|
39407
|
+
remote_branch_deleted: false,
|
|
39408
|
+
warn: `Remote branch delete failed (non-fatal): git push origin --delete ${branchName}: ${errMsg}`
|
|
39409
|
+
});
|
|
39410
|
+
}
|
|
39411
|
+
return Promise.resolve({ remote_branch_deleted: true });
|
|
39412
|
+
}
|
|
39413
|
+
function defaultDeleteFeatBranch(branchName, cwd, remote_is_gone) {
|
|
39414
|
+
const gitRun = defaultGitRun2;
|
|
39415
|
+
const safeResult = gitRun(["branch", "-d", branchName], cwd);
|
|
39416
|
+
if (safeResult.status === 0) {
|
|
39417
|
+
return Promise.resolve({ local_branch_deleted: true });
|
|
39418
|
+
}
|
|
39419
|
+
const safeErrMsg = safeResult.stderr.trim() || safeResult.error?.message || "unknown error";
|
|
39420
|
+
if (remote_is_gone) {
|
|
39421
|
+
const forceResult = gitRun(["branch", "-D", branchName], cwd);
|
|
39422
|
+
if (forceResult.status === 0) {
|
|
39423
|
+
return Promise.resolve({ local_branch_deleted: true });
|
|
39424
|
+
}
|
|
39425
|
+
const forceErrMsg = forceResult.stderr.trim() || forceResult.error?.message || "unknown error";
|
|
39426
|
+
return Promise.resolve({
|
|
39427
|
+
local_branch_deleted: false,
|
|
39428
|
+
warn: `Local branch force-delete failed (non-fatal): git branch -D ${branchName}: ${forceErrMsg}`
|
|
39429
|
+
});
|
|
39430
|
+
}
|
|
39431
|
+
return Promise.resolve({
|
|
39432
|
+
local_branch_deleted: false,
|
|
39433
|
+
warn: `Local branch delete failed (non-fatal, remote still present \u2014 skipping force-delete): git branch -d ${branchName}: ${safeErrMsg}`
|
|
39434
|
+
});
|
|
39435
|
+
}
|
|
39390
39436
|
function parseChkNumber(name) {
|
|
39391
39437
|
const chkMatch = /^CHK-(\d+)$/i.exec(name);
|
|
39392
39438
|
if (chkMatch?.[1]) {
|
|
@@ -39399,12 +39445,23 @@ function parseChkNumber(name) {
|
|
|
39399
39445
|
}
|
|
39400
39446
|
return null;
|
|
39401
39447
|
}
|
|
39402
|
-
async function
|
|
39448
|
+
async function runWorktreeRemoveAtPath(worktreePath, deps) {
|
|
39403
39449
|
const cwd = deps.cwd ?? process.cwd();
|
|
39404
39450
|
const gitRun = deps.gitRun ?? defaultGitRun2;
|
|
39405
39451
|
const getWorktreeBranchName = deps.getWorktreeBranchName ?? defaultGetWorktreeBranchName;
|
|
39406
39452
|
const deleteSupabaseBranch = deps.deleteSupabaseBranch ?? defaultDeleteSupabaseBranch;
|
|
39407
|
-
const
|
|
39453
|
+
const isInsideWorktreePath = deps.isInsideWorktreePath ?? defaultIsInsideWorktreePath;
|
|
39454
|
+
const verifyAndDeleteRemoteBranch = deps.verifyAndDeleteRemoteBranch ?? defaultVerifyAndDeleteRemoteBranch;
|
|
39455
|
+
const deleteFeatBranch = deps.deleteFeatBranch ?? defaultDeleteFeatBranch;
|
|
39456
|
+
if (isInsideWorktreePath(worktreePath, cwd)) {
|
|
39457
|
+
process.stderr.write(
|
|
39458
|
+
JSON.stringify({
|
|
39459
|
+
error: "session is inside the target worktree",
|
|
39460
|
+
directive: `Switch to your main checkout, then re-run: codebyplan worktree remove ${worktreePath}`
|
|
39461
|
+
}) + "\n"
|
|
39462
|
+
);
|
|
39463
|
+
return 2;
|
|
39464
|
+
}
|
|
39408
39465
|
const warnings = [];
|
|
39409
39466
|
const branchName = getWorktreeBranchName(worktreePath);
|
|
39410
39467
|
const removeResult = gitRun(["worktree", "remove", worktreePath], cwd);
|
|
@@ -39425,15 +39482,45 @@ async function runWorktreeRemoveChk(checkpointNumber, deps) {
|
|
|
39425
39482
|
`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.`
|
|
39426
39483
|
);
|
|
39427
39484
|
}
|
|
39485
|
+
let remote_branch_deleted;
|
|
39486
|
+
let local_branch_deleted;
|
|
39487
|
+
let remote_is_gone = false;
|
|
39488
|
+
if (branchName && git_removed) {
|
|
39489
|
+
const remoteResult = await verifyAndDeleteRemoteBranch(branchName, cwd);
|
|
39490
|
+
remote_branch_deleted = remoteResult.remote_branch_deleted;
|
|
39491
|
+
if (remoteResult.warn) {
|
|
39492
|
+
warnings.push(remoteResult.warn);
|
|
39493
|
+
}
|
|
39494
|
+
remote_is_gone = remote_branch_deleted === true || remote_branch_deleted === false && !remoteResult.warn;
|
|
39495
|
+
const localResult = await deleteFeatBranch(branchName, cwd, remote_is_gone);
|
|
39496
|
+
local_branch_deleted = localResult.local_branch_deleted;
|
|
39497
|
+
if (localResult.warn) {
|
|
39498
|
+
warnings.push(localResult.warn);
|
|
39499
|
+
}
|
|
39500
|
+
} else if (branchName && !git_removed) {
|
|
39501
|
+
warnings.push(
|
|
39502
|
+
`Skipped remote/local branch cleanup for '${branchName}': the worktree at '${worktreePath}' was not removed.`
|
|
39503
|
+
);
|
|
39504
|
+
}
|
|
39428
39505
|
const result = {
|
|
39429
39506
|
mcp_deregistered: false,
|
|
39430
39507
|
git_removed,
|
|
39431
39508
|
supabase_torn_down,
|
|
39509
|
+
...remote_branch_deleted !== void 0 ? { remote_branch_deleted } : {},
|
|
39510
|
+
...local_branch_deleted !== void 0 ? { local_branch_deleted } : {},
|
|
39432
39511
|
...warnings.length > 0 ? { warn: warnings.join("; ") } : {}
|
|
39433
39512
|
};
|
|
39434
39513
|
process.stdout.write(JSON.stringify(result) + "\n");
|
|
39435
39514
|
return 0;
|
|
39436
39515
|
}
|
|
39516
|
+
async function runWorktreeRemoveChk(checkpointNumber, deps) {
|
|
39517
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
39518
|
+
const worktreePath = computeWorktreePath(cwd, checkpointNumber);
|
|
39519
|
+
return runWorktreeRemoveAtPath(worktreePath, deps);
|
|
39520
|
+
}
|
|
39521
|
+
async function runWorktreeRemoveByPath(worktreePath, deps) {
|
|
39522
|
+
return runWorktreeRemoveAtPath(worktreePath, deps);
|
|
39523
|
+
}
|
|
39437
39524
|
async function runWorktreeRemove(args, deps = {}) {
|
|
39438
39525
|
const name = args.find((a) => !a.startsWith("--"));
|
|
39439
39526
|
if (!name) {
|
|
@@ -39444,6 +39531,9 @@ async function runWorktreeRemove(args, deps = {}) {
|
|
|
39444
39531
|
);
|
|
39445
39532
|
return 1;
|
|
39446
39533
|
}
|
|
39534
|
+
if (isAbsolute4(name)) {
|
|
39535
|
+
return runWorktreeRemoveByPath(name, deps);
|
|
39536
|
+
}
|
|
39447
39537
|
const chkNumber = parseChkNumber(name);
|
|
39448
39538
|
if (chkNumber !== null) {
|
|
39449
39539
|
return runWorktreeRemoveChk(chkNumber, deps);
|
|
@@ -41244,6 +41334,45 @@ var init_verify_parity2 = __esm({
|
|
|
41244
41334
|
}
|
|
41245
41335
|
});
|
|
41246
41336
|
|
|
41337
|
+
// src/lib/tech-stack-render.ts
|
|
41338
|
+
function buildTechStackDisplay(flat) {
|
|
41339
|
+
const categoryMap = {};
|
|
41340
|
+
for (const entry of flat) {
|
|
41341
|
+
if (entry.name === SYNTHETIC_CARRIER_NAME) continue;
|
|
41342
|
+
if (!categoryMap[entry.category]) {
|
|
41343
|
+
categoryMap[entry.category] = [];
|
|
41344
|
+
}
|
|
41345
|
+
categoryMap[entry.category].push(entry.name);
|
|
41346
|
+
}
|
|
41347
|
+
if ((categoryMap["mobile"]?.length ?? 0) > 0) {
|
|
41348
|
+
const frameworkEntries = categoryMap["framework"] ?? [];
|
|
41349
|
+
const filtered = frameworkEntries.filter((n) => n !== "React");
|
|
41350
|
+
if (filtered.length > 0) {
|
|
41351
|
+
categoryMap["framework"] = filtered;
|
|
41352
|
+
} else {
|
|
41353
|
+
delete categoryMap["framework"];
|
|
41354
|
+
}
|
|
41355
|
+
}
|
|
41356
|
+
const techStack = {};
|
|
41357
|
+
for (const [cat, names] of Object.entries(categoryMap)) {
|
|
41358
|
+
const label = CATEGORY_LABELS[cat] ?? cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
41359
|
+
techStack[label] = [...names].sort().join(" + ");
|
|
41360
|
+
}
|
|
41361
|
+
return { categoryMap, techStack };
|
|
41362
|
+
}
|
|
41363
|
+
var CATEGORY_LABELS;
|
|
41364
|
+
var init_tech_stack_render = __esm({
|
|
41365
|
+
"src/lib/tech-stack-render.ts"() {
|
|
41366
|
+
"use strict";
|
|
41367
|
+
init_tech_detect();
|
|
41368
|
+
CATEGORY_LABELS = {
|
|
41369
|
+
"component-lib": "Component Library",
|
|
41370
|
+
graphql: "GraphQL",
|
|
41371
|
+
quality: "Code Quality"
|
|
41372
|
+
};
|
|
41373
|
+
}
|
|
41374
|
+
});
|
|
41375
|
+
|
|
41247
41376
|
// src/lib/structure-generator.ts
|
|
41248
41377
|
function renderTechStack(techStack) {
|
|
41249
41378
|
const entries = Object.entries(techStack).filter(([, value]) => value.trim().length > 0).sort(([a], [b]) => a.localeCompare(b));
|
|
@@ -41731,28 +41860,7 @@ async function runGenerate(opts) {
|
|
|
41731
41860
|
});
|
|
41732
41861
|
}
|
|
41733
41862
|
const techResult = await detectTechStack(rootDir);
|
|
41734
|
-
const
|
|
41735
|
-
"component-lib": "Component Library",
|
|
41736
|
-
graphql: "GraphQL",
|
|
41737
|
-
quality: "Code Quality"
|
|
41738
|
-
};
|
|
41739
|
-
const categoryMap = {};
|
|
41740
|
-
for (const entry of techResult.flat) {
|
|
41741
|
-
if (entry.name === SYNTHETIC_CARRIER_NAME) continue;
|
|
41742
|
-
if (!categoryMap[entry.category]) {
|
|
41743
|
-
categoryMap[entry.category] = [];
|
|
41744
|
-
}
|
|
41745
|
-
categoryMap[entry.category].push(entry.name);
|
|
41746
|
-
}
|
|
41747
|
-
if ((categoryMap["mobile"]?.length ?? 0) > 0) {
|
|
41748
|
-
const frameworkEntries = categoryMap["framework"] ?? [];
|
|
41749
|
-
const filtered = frameworkEntries.filter((n) => n !== "React");
|
|
41750
|
-
if (filtered.length > 0) {
|
|
41751
|
-
categoryMap["framework"] = filtered;
|
|
41752
|
-
} else {
|
|
41753
|
-
delete categoryMap["framework"];
|
|
41754
|
-
}
|
|
41755
|
-
}
|
|
41863
|
+
const { categoryMap, techStack } = buildTechStackDisplay(techResult.flat);
|
|
41756
41864
|
const hasNextjs = (categoryMap["framework"] ?? []).some(
|
|
41757
41865
|
(n) => n.toLowerCase().includes("next")
|
|
41758
41866
|
);
|
|
@@ -41760,11 +41868,6 @@ async function runGenerate(opts) {
|
|
|
41760
41868
|
if (p.server_type === "nextjs" && !hasNextjs) return false;
|
|
41761
41869
|
return true;
|
|
41762
41870
|
});
|
|
41763
|
-
const techStack = {};
|
|
41764
|
-
for (const [cat, names] of Object.entries(categoryMap)) {
|
|
41765
|
-
const label = CATEGORY_LABELS[cat] ?? cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
41766
|
-
techStack[label] = [...names].sort().join(" + ");
|
|
41767
|
-
}
|
|
41768
41871
|
const config = {
|
|
41769
41872
|
techStack: Object.keys(techStack).length > 0 ? techStack : void 0,
|
|
41770
41873
|
packageManager,
|
|
@@ -41799,6 +41902,25 @@ async function runGenerate(opts) {
|
|
|
41799
41902
|
`);
|
|
41800
41903
|
}
|
|
41801
41904
|
}
|
|
41905
|
+
const structurePath = join56(rootDir, ".claude", "generated", "structure.md");
|
|
41906
|
+
let existingStructureCheck = null;
|
|
41907
|
+
try {
|
|
41908
|
+
existingStructureCheck = await readFile30(structurePath, "utf-8");
|
|
41909
|
+
} catch {
|
|
41910
|
+
existingStructureCheck = null;
|
|
41911
|
+
}
|
|
41912
|
+
if (existingStructureCheck === null) {
|
|
41913
|
+
process.stdout.write(`structure.md missing
|
|
41914
|
+
`);
|
|
41915
|
+
process.exitCode = 1;
|
|
41916
|
+
} else if (existingStructureCheck !== structureMdContent) {
|
|
41917
|
+
process.stdout.write(`structure.md drifted
|
|
41918
|
+
`);
|
|
41919
|
+
process.exitCode = 1;
|
|
41920
|
+
} else {
|
|
41921
|
+
process.stdout.write(`.claude/generated/structure.md up to date
|
|
41922
|
+
`);
|
|
41923
|
+
}
|
|
41802
41924
|
return;
|
|
41803
41925
|
}
|
|
41804
41926
|
if (dryRun) {
|
|
@@ -41818,9 +41940,20 @@ async function runGenerate(opts) {
|
|
|
41818
41940
|
const outputDir = join56(rootDir, ".claude", "generated");
|
|
41819
41941
|
await mkdir14(outputDir, { recursive: true });
|
|
41820
41942
|
const outputPath = join56(outputDir, "structure.md");
|
|
41821
|
-
|
|
41822
|
-
|
|
41943
|
+
let existingStructure = null;
|
|
41944
|
+
try {
|
|
41945
|
+
existingStructure = await readFile30(outputPath, "utf-8");
|
|
41946
|
+
} catch {
|
|
41947
|
+
existingStructure = null;
|
|
41948
|
+
}
|
|
41949
|
+
if (existingStructure !== null && existingStructure === structureMdContent) {
|
|
41950
|
+
process.stdout.write(`Up to date: .claude/generated/structure.md
|
|
41951
|
+
`);
|
|
41952
|
+
} else {
|
|
41953
|
+
await writeFile20(outputPath, structureMdContent, "utf-8");
|
|
41954
|
+
process.stdout.write(`Wrote: .claude/generated/structure.md
|
|
41823
41955
|
`);
|
|
41956
|
+
}
|
|
41824
41957
|
const agentsMdPath = join56(rootDir, "AGENTS.md");
|
|
41825
41958
|
let existingAgentsContent = null;
|
|
41826
41959
|
try {
|
|
@@ -41841,6 +41974,7 @@ var init_generate = __esm({
|
|
|
41841
41974
|
"src/cli/claude/generate.ts"() {
|
|
41842
41975
|
"use strict";
|
|
41843
41976
|
init_tech_detect();
|
|
41977
|
+
init_tech_stack_render();
|
|
41844
41978
|
init_structure_generator();
|
|
41845
41979
|
init_agents_generator();
|
|
41846
41980
|
}
|
|
@@ -41866,34 +42000,9 @@ async function buildConfig(absPath, isRoot, allPackages, rootPkgJson, pkgJson) {
|
|
|
41866
42000
|
let techStack;
|
|
41867
42001
|
try {
|
|
41868
42002
|
const techResult = await detectTechStack(absPath);
|
|
41869
|
-
const
|
|
41870
|
-
|
|
41871
|
-
|
|
41872
|
-
quality: "Code Quality"
|
|
41873
|
-
};
|
|
41874
|
-
const categoryMap = {};
|
|
41875
|
-
for (const entry of techResult.flat) {
|
|
41876
|
-
if (entry.name === SYNTHETIC_CARRIER_NAME) continue;
|
|
41877
|
-
if (!categoryMap[entry.category]) {
|
|
41878
|
-
categoryMap[entry.category] = [];
|
|
41879
|
-
}
|
|
41880
|
-
categoryMap[entry.category].push(entry.name);
|
|
41881
|
-
}
|
|
41882
|
-
if ((categoryMap["mobile"]?.length ?? 0) > 0) {
|
|
41883
|
-
const frameworkEntries = categoryMap["framework"] ?? [];
|
|
41884
|
-
const filtered = frameworkEntries.filter((n) => n !== "React");
|
|
41885
|
-
if (filtered.length > 0) {
|
|
41886
|
-
categoryMap["framework"] = filtered;
|
|
41887
|
-
} else {
|
|
41888
|
-
delete categoryMap["framework"];
|
|
41889
|
-
}
|
|
41890
|
-
}
|
|
41891
|
-
if (Object.keys(categoryMap).length > 0) {
|
|
41892
|
-
techStack = {};
|
|
41893
|
-
for (const [cat, names] of Object.entries(categoryMap)) {
|
|
41894
|
-
const label = CATEGORY_LABELS[cat] ?? cat.charAt(0).toUpperCase() + cat.slice(1);
|
|
41895
|
-
techStack[label] = [...names].sort().join(" + ");
|
|
41896
|
-
}
|
|
42003
|
+
const { techStack: ts } = buildTechStackDisplay(techResult.flat);
|
|
42004
|
+
if (Object.keys(ts).length > 0) {
|
|
42005
|
+
techStack = ts;
|
|
41897
42006
|
}
|
|
41898
42007
|
} catch {
|
|
41899
42008
|
}
|
|
@@ -42128,6 +42237,7 @@ var init_readme = __esm({
|
|
|
42128
42237
|
"src/cli/claude/readme.ts"() {
|
|
42129
42238
|
"use strict";
|
|
42130
42239
|
init_tech_detect();
|
|
42240
|
+
init_tech_stack_render();
|
|
42131
42241
|
init_readme_generator();
|
|
42132
42242
|
}
|
|
42133
42243
|
});
|
|
@@ -42152,7 +42262,7 @@ import {
|
|
|
42152
42262
|
readdir as readdir9
|
|
42153
42263
|
} from "node:fs/promises";
|
|
42154
42264
|
import { existsSync as existsSync21 } from "node:fs";
|
|
42155
|
-
import { join as join58, resolve as resolve15, dirname as dirname17, sep as
|
|
42265
|
+
import { join as join58, resolve as resolve15, dirname as dirname17, sep as sep5 } from "node:path";
|
|
42156
42266
|
import { homedir as homedir8 } from "node:os";
|
|
42157
42267
|
function encodeProjectPath(absPath) {
|
|
42158
42268
|
return resolve15(absPath).replace(/[/\\]/g, "-");
|
|
@@ -42320,7 +42430,7 @@ async function applyPlan(plan, opts) {
|
|
|
42320
42430
|
const relPath = entry.suggested_target.slice("nested:".length);
|
|
42321
42431
|
const targetDir = resolve15(join58(projectDir, relPath));
|
|
42322
42432
|
const targetFile = join58(targetDir, "CLAUDE.md");
|
|
42323
|
-
if (!targetDir.startsWith(resolve15(projectDir) +
|
|
42433
|
+
if (!targetDir.startsWith(resolve15(projectDir) + sep5)) {
|
|
42324
42434
|
process.stderr.write(
|
|
42325
42435
|
`migrate-memory: skipping unsafe suggested_target "${entry.suggested_target}" \u2014 resolves outside projectDir
|
|
42326
42436
|
`
|
|
@@ -42339,7 +42449,7 @@ ${anchor}
|
|
|
42339
42449
|
process.stdout.write(`[dry-run] Would create/append: ${targetFile}
|
|
42340
42450
|
`);
|
|
42341
42451
|
if (resolve15(entry.source_path).startsWith(
|
|
42342
|
-
resolve15(plan.auto_memory_dir) +
|
|
42452
|
+
resolve15(plan.auto_memory_dir) + sep5
|
|
42343
42453
|
)) {
|
|
42344
42454
|
process.stdout.write(
|
|
42345
42455
|
`[dry-run] Would delete migrated keep source: ${entry.source_path}
|
|
@@ -42357,7 +42467,7 @@ ${anchor}
|
|
|
42357
42467
|
if (!existing.includes(anchor)) {
|
|
42358
42468
|
await writeFile22(targetFile, existing + appendContent, "utf-8");
|
|
42359
42469
|
}
|
|
42360
|
-
if (resolve15(entry.source_path).startsWith(resolve15(plan.auto_memory_dir) +
|
|
42470
|
+
if (resolve15(entry.source_path).startsWith(resolve15(plan.auto_memory_dir) + sep5)) {
|
|
42361
42471
|
try {
|
|
42362
42472
|
await unlink8(entry.source_path);
|
|
42363
42473
|
} catch {
|
|
@@ -42395,7 +42505,7 @@ ${IMPORT_LINE}
|
|
|
42395
42505
|
for (const entry of plan.entries) {
|
|
42396
42506
|
if (entry.suggested_action !== "drop") continue;
|
|
42397
42507
|
if (!resolve15(entry.source_path).startsWith(
|
|
42398
|
-
resolve15(plan.auto_memory_dir) +
|
|
42508
|
+
resolve15(plan.auto_memory_dir) + sep5
|
|
42399
42509
|
)) {
|
|
42400
42510
|
process.stderr.write(
|
|
42401
42511
|
`migrate-memory: skipping delete of "${entry.source_path}" \u2014 resolves outside auto_memory_dir
|
|
@@ -42419,7 +42529,7 @@ ${IMPORT_LINE}
|
|
|
42419
42529
|
process.stdout.write(`[dry-run] Would delete MEMORY.md: ${memoryMd}
|
|
42420
42530
|
`);
|
|
42421
42531
|
} else {
|
|
42422
|
-
if (resolve15(plan.auto_memory_dir).startsWith(safeRmdirBase +
|
|
42532
|
+
if (resolve15(plan.auto_memory_dir).startsWith(safeRmdirBase + sep5)) {
|
|
42423
42533
|
try {
|
|
42424
42534
|
await unlink8(memoryMd);
|
|
42425
42535
|
} catch {
|
|
@@ -42437,7 +42547,7 @@ ${IMPORT_LINE}
|
|
|
42437
42547
|
`
|
|
42438
42548
|
);
|
|
42439
42549
|
} else {
|
|
42440
|
-
if (!resolve15(plan.auto_memory_dir).startsWith(safeRmdirBase +
|
|
42550
|
+
if (!resolve15(plan.auto_memory_dir).startsWith(safeRmdirBase + sep5)) {
|
|
42441
42551
|
process.stderr.write(
|
|
42442
42552
|
`migrate-memory: skipping rmdir of "${plan.auto_memory_dir}" \u2014 not under ~/.claude/projects
|
|
42443
42553
|
`
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, I/O-free helper for rendering a flat TechStackEntry[] into the
|
|
3
|
+
* display structures used by `codebyplan claude generate` and
|
|
4
|
+
* `codebyplan claude readme`.
|
|
5
|
+
*
|
|
6
|
+
* Both callers previously contained identical copies of this logic.
|
|
7
|
+
* Extracted here so changes propagate uniformly and the logic can be
|
|
8
|
+
* unit-tested in isolation.
|
|
9
|
+
*/
|
|
10
|
+
import type { TechStackEntry } from "./types.js";
|
|
11
|
+
/** Human-readable labels for multi-word category keys. */
|
|
12
|
+
export declare const CATEGORY_LABELS: Record<string, string>;
|
|
13
|
+
export interface TechStackRenderResult {
|
|
14
|
+
/** Per-category name lists, post-F5 mobile guard. */
|
|
15
|
+
categoryMap: Record<string, string[]>;
|
|
16
|
+
/** Display-ready map: label → sorted joined names (e.g. "Framework" → "Next.js + React"). */
|
|
17
|
+
techStack: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build category and display maps from a flat TechStackEntry array.
|
|
21
|
+
*
|
|
22
|
+
* Behaviour:
|
|
23
|
+
* 1. Iterates `flat`, skipping entries where `entry.name === SYNTHETIC_CARRIER_NAME`.
|
|
24
|
+
* 2. F5 mobile guard: when `categoryMap["mobile"]` is non-empty, strips "React"
|
|
25
|
+
* from `categoryMap["framework"]`; deletes the key entirely if that leaves it empty.
|
|
26
|
+
* 3. Builds `techStack` by joining sorted names per category under a human-readable label.
|
|
27
|
+
* 4. Empty input → `{ categoryMap: {}, techStack: {} }`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function buildTechStackDisplay(flat: TechStackEntry[]): TechStackRenderResult;
|
|
30
|
+
//# sourceMappingURL=tech-stack-render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tech-stack-render.d.ts","sourceRoot":"","sources":["../../src/lib/tech-stack-render.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,0DAA0D;AAC1D,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAIlD,CAAC;AAEF,MAAM,WAAW,qBAAqB;IACpC,qDAAqD;IACrD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACtC,6FAA6F;IAC7F,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,cAAc,EAAE,GACrB,qBAAqB,CAgCvB"}
|
package/package.json
CHANGED
|
@@ -374,87 +374,6 @@ fi
|
|
|
374
374
|
|
|
375
375
|
echo ""
|
|
376
376
|
|
|
377
|
-
# ===== HOOK SMOKE TESTS — cbp-mcp-caller-worktree-inject =====
|
|
378
|
-
echo "## Hook Smoke Tests — cbp-mcp-caller-worktree-inject (CHK-198)"
|
|
379
|
-
|
|
380
|
-
INJECT_HOOK="$HOOKS_DIR/cbp-mcp-caller-worktree-inject.sh"
|
|
381
|
-
# Absolute path — the fail-open test runs the hook from a temp cwd (to isolate it
|
|
382
|
-
# from this repo's git context), where the relative "$HOOKS_DIR" no longer resolves.
|
|
383
|
-
INJECT_HOOK_ABS="$(cd "$HOOKS_DIR" 2>/dev/null && pwd)/cbp-mcp-caller-worktree-inject.sh"
|
|
384
|
-
|
|
385
|
-
if [ ! -f "$INJECT_HOOK" ]; then
|
|
386
|
-
test_result "cbp-mcp-caller-worktree-inject.sh present" "passed" "missing"
|
|
387
|
-
else
|
|
388
|
-
test_result "cbp-mcp-caller-worktree-inject.sh present" "passed" "passed"
|
|
389
|
-
|
|
390
|
-
FIRST_LINE=$(head -1 "$INJECT_HOOK")
|
|
391
|
-
if echo "$FIRST_LINE" | grep -q '^#!/'; then
|
|
392
|
-
test_result "cbp-mcp-caller-worktree-inject.sh has shebang" "passed" "passed"
|
|
393
|
-
else
|
|
394
|
-
test_result "cbp-mcp-caller-worktree-inject.sh has shebang" "passed" "missing"
|
|
395
|
-
fi
|
|
396
|
-
|
|
397
|
-
if grep -q '@scope: org-shared' "$INJECT_HOOK"; then
|
|
398
|
-
test_result "cbp-mcp-caller-worktree-inject.sh has @scope: org-shared" "passed" "passed"
|
|
399
|
-
else
|
|
400
|
-
test_result "cbp-mcp-caller-worktree-inject.sh has @scope: org-shared" "passed" "missing"
|
|
401
|
-
fi
|
|
402
|
-
|
|
403
|
-
# Fail-open: run from a non-repo temp dir with no worktree cache and no
|
|
404
|
-
# CLAUDE_PROJECT_DIR — neither the cache nor the CLI fallback can resolve a
|
|
405
|
-
# worktree, so the hook must exit 0 with empty stdout (no updatedInput).
|
|
406
|
-
ISO=$(mktemp -d)
|
|
407
|
-
OUTPUT=$( (cd "$ISO" && env -u CLAUDE_PROJECT_DIR bash "$INJECT_HOOK_ABS" <<< '{"tool_input":{"task_id":"x"}}') 2>/dev/null )
|
|
408
|
-
EXIT_CODE=$?
|
|
409
|
-
if [ "$EXIT_CODE" = "0" ] && [ -z "$OUTPUT" ]; then
|
|
410
|
-
test_result "cbp-mcp-caller-worktree-inject.sh fail-open (unresolvable) exits 0 + empty stdout" "passed" "passed"
|
|
411
|
-
else
|
|
412
|
-
test_result "cbp-mcp-caller-worktree-inject.sh fail-open (unresolvable) exits 0 + empty stdout" "passed" "failed (exit=$EXIT_CODE)"
|
|
413
|
-
fi
|
|
414
|
-
rm -rf "$ISO"
|
|
415
|
-
|
|
416
|
-
# C6 — input already carries a non-empty caller_worktree_id → never overwrite;
|
|
417
|
-
# early-return with exit 0 and empty stdout (no resolution attempted).
|
|
418
|
-
OUTPUT=$(echo '{"tool_input":{"caller_worktree_id":"11111111-1111-1111-1111-111111111111"}}' | bash "$INJECT_HOOK" 2>/dev/null)
|
|
419
|
-
EXIT_CODE=$?
|
|
420
|
-
if [ "$EXIT_CODE" = "0" ] && [ -z "$OUTPUT" ]; then
|
|
421
|
-
test_result "cbp-mcp-caller-worktree-inject.sh C6 keeps existing caller_worktree_id (exit 0 + empty stdout)" "passed" "passed"
|
|
422
|
-
else
|
|
423
|
-
test_result "cbp-mcp-caller-worktree-inject.sh C6 keeps existing caller_worktree_id (exit 0 + empty stdout)" "passed" "failed (exit=$EXIT_CODE)"
|
|
424
|
-
fi
|
|
425
|
-
|
|
426
|
-
# Injection — a worktree.local.json whose .branch matches the current git branch
|
|
427
|
-
# makes the cache fast-path resolve. Use a synthetic UUID so the assertion proves
|
|
428
|
-
# the cache value (not the live CLI) was injected. Skipped when no concrete git
|
|
429
|
-
# branch resolves (detached HEAD / non-git checkout) or jq is unavailable.
|
|
430
|
-
CUR_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
|
431
|
-
if [ -n "$CUR_BRANCH" ] && [ "$CUR_BRANCH" != "HEAD" ] && command -v jq >/dev/null 2>&1; then
|
|
432
|
-
ISO=$(mktemp -d)
|
|
433
|
-
mkdir -p "$ISO/.codebyplan"
|
|
434
|
-
FAKE_WT="abcdef01-2345-6789-abcd-ef0123456789"
|
|
435
|
-
jq -n --arg b "$CUR_BRANCH" --arg w "$FAKE_WT" \
|
|
436
|
-
'{worktree_id:$w, branch:$b}' > "$ISO/.codebyplan/worktree.local.json"
|
|
437
|
-
OUTPUT=$(CLAUDE_PROJECT_DIR="$ISO" bash "$INJECT_HOOK" <<< '{"tool_input":{"task_id":"x"}}' 2>/dev/null)
|
|
438
|
-
EXIT_CODE=$?
|
|
439
|
-
INJECTED=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.updatedInput.caller_worktree_id // empty' 2>/dev/null)
|
|
440
|
-
# Sibling-key survival — CC's updatedInput REPLACES tool_input wholesale (it is
|
|
441
|
-
# not a partial merge), so the hook must echo back every original field merged
|
|
442
|
-
# with caller_worktree_id. Assert the non-target sibling key (task_id) survives;
|
|
443
|
-
# this is the assertion gap that let the replace-vs-merge bug ship in round 2.
|
|
444
|
-
PRESERVED=$(echo "$OUTPUT" | jq -r '.hookSpecificOutput.updatedInput.task_id // empty' 2>/dev/null)
|
|
445
|
-
if [ "$EXIT_CODE" = "0" ] && [ "$INJECTED" = "$FAKE_WT" ] && [ "$PRESERVED" = "x" ]; then
|
|
446
|
-
test_result "cbp-mcp-caller-worktree-inject.sh injects caller_worktree_id AND preserves sibling keys" "passed" "passed"
|
|
447
|
-
else
|
|
448
|
-
test_result "cbp-mcp-caller-worktree-inject.sh injects caller_worktree_id AND preserves sibling keys" "passed" "failed (exit=$EXIT_CODE injected=$INJECTED preserved=$PRESERVED)"
|
|
449
|
-
fi
|
|
450
|
-
rm -rf "$ISO"
|
|
451
|
-
else
|
|
452
|
-
test_result "cbp-mcp-caller-worktree-inject.sh injection test (no branch resolvable — skipped)" "passed" "passed"
|
|
453
|
-
fi
|
|
454
|
-
fi
|
|
455
|
-
|
|
456
|
-
echo ""
|
|
457
|
-
|
|
458
377
|
# ===== HOOK SMOKE TESTS — cbp-session-start-hook =====
|
|
459
378
|
echo "## Hook Smoke Tests — cbp-session-start-hook (CHK-178)"
|
|
460
379
|
|
|
@@ -25,11 +25,13 @@ SHARED tooling behavior only — repo-specific gotchas belong in that repo's own
|
|
|
25
25
|
clobbers existing `decisions` / `discoveries` / `check_results`. Always read the current row,
|
|
26
26
|
merge your change into the full object/array, then write the whole thing back.
|
|
27
27
|
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
- **User-level locks are invisible until a mutation they block.** `get_checkpoints` /
|
|
29
|
+
`get_tasks` succeed even when another user holds the assignment; the 403 fires only on
|
|
30
|
+
`update_*` / `complete_*`. The lock keys on the JWT user (`ctx.userId`) vs the row's
|
|
31
|
+
`assigned_user_id` (null = open). `caller_worktree_id` / `worktree_id` params are
|
|
32
|
+
accepted-and-ignored — do not thread them. Verify `assigned_user_id` matches
|
|
33
|
+
`npx codebyplan whoami` before mutating; recover a stale assignment with
|
|
34
|
+
`release_assignment` (maintainer).
|
|
33
35
|
|
|
34
36
|
- **Full-repo lint/type baselines are often pre-existing red.** A round must gate on the files
|
|
35
37
|
it changed, not the whole-repo baseline — scope lint/tsc checks to the round's changed set so a
|
|
@@ -42,14 +44,10 @@ SHARED tooling behavior only — repo-specific gotchas belong in that repo's own
|
|
|
42
44
|
files or carried directory-slash round artifacts, `complete_task` can hard-fail "N files not
|
|
43
45
|
approved"; fix by re-writing each affected round's `files_changed` via `update_round`.
|
|
44
46
|
|
|
45
|
-
- **CLI transport uses REST (reads) and OAuth+MCP (writes) — a 502 from `codebyplan round sync-approvals` is transient MCP churn, not an outage.** The CLI exits 0 with a warning and MCP tools still work. A missing `CODEBYPLAN_API_KEY` surfaces as an `ApiError`, not a 502. `sync-approvals` can also drag untracked per-device dirs into `files_changed` — run it from the repo root
|
|
47
|
+
- **CLI transport uses REST (reads) and OAuth+MCP (writes) — a 502 from `codebyplan round sync-approvals` is transient MCP churn, not an outage.** The CLI exits 0 with a warning and MCP tools still work. A missing `CODEBYPLAN_API_KEY` surfaces as an `ApiError`, not a 502. `sync-approvals` can also drag untracked per-device dirs into `files_changed` — run it from the repo root.
|
|
46
48
|
|
|
47
49
|
- **`codebyplan claude update` requires a TTY.** On non-TTY stdin (CI, piped) it half-applies then errors. Re-run with `--yes` to accept defaults non-interactively.
|
|
48
50
|
|
|
49
|
-
- **Checkpoint locks are invisible until a mutation they block.** `get_checkpoints` / `get_tasks` succeed even when another worktree holds the lock; the 403 fires only on `update_*` / `complete_*`. Verify the row's `worktree_id` matches the caller before mutating. A null-`worktree_id` checkpoint can still be actively shipped by whichever worktree physically holds its feat branch — check `git worktree list` first.
|
|
50
|
-
|
|
51
|
-
- **`update_task` accepts `caller_worktree_id` for lock-verify only — it does NOT assign ownership.** Ownership assignment goes through the web UI or the dedicated assignment path. Don't conflate `caller_worktree_id` with `assigned_worktree_id`.
|
|
52
|
-
|
|
53
51
|
- **Re-run config-driven gates after merging main into a feat branch.** A merge can add or change `.codebyplan/shipment.json`, ports, branch config, `e2e.json`, and `eslint.json` — treat the post-merge state as a fresh baseline before continuing.
|
|
54
52
|
|
|
55
53
|
- **MCP write calls can return 403 via Cloudflare WAF when the JSONB payload contains DDL
|
|
@@ -20,8 +20,8 @@ Defined in `supabase/migrations/20260511211900_chk111_workflow_invariants.sql` (
|
|
|
20
20
|
| 2 | `trg_enforce_standalone_task_workflow_invariants` | A standalone task cannot be moved to `in_progress` without `assigned_user_id` (CHK-225: was `assigned_worktree_id`) |
|
|
21
21
|
| 3 | `trg_enforce_task_workflow_invariants` | ≤ 1 `in_progress` task per checkpoint |
|
|
22
22
|
| 4 | `trg_enforce_single_in_progress_round_per_task` | ≤ 1 `in_progress` round per task |
|
|
23
|
-
| 5 | `trg_enforce_single_active_scope_per_worktree` | ≤ 1 active (checkpoint OR standalone task) per `
|
|
24
|
-
| 6 | `trg_enforce_standalone_task_scope_per_worktree` | ≤ 1 `in_progress` standalone task per `
|
|
23
|
+
| 5 | `trg_enforce_single_active_scope_per_worktree` | ≤ 1 active (checkpoint OR standalone task) per `branch_name` (CHK-235: was per `assigned_user_id`) |
|
|
24
|
+
| 6 | `trg_enforce_standalone_task_scope_per_worktree` | ≤ 1 `in_progress` standalone task per `branch_name` (CHK-235: was per `assigned_user_id`) |
|
|
25
25
|
|
|
26
26
|
The worker is a passive cross-checker (`apps/todo-worker/src/invariants/check.ts`) — if its check disagrees with the DB, the DB wins.
|
|
27
27
|
|
|
@@ -109,6 +109,8 @@ CHK-111 shipped the original todos queue as Postgres triggers + a 583-LOC `regen
|
|
|
109
109
|
|
|
110
110
|
CHK-225 updated the invariant triggers from worktree-scoped to user-scoped (`assigned_user_id`). The trigger names were preserved for continuity; only the function bodies changed. Migration: `20260612000000_chk225_task1_user_locks.sql`.
|
|
111
111
|
|
|
112
|
+
CHK-235 re-scoped the same two trigger functions from per-`assigned_user_id` to per-`branch_name`, restoring multi-worktree concurrency (one active scope per feat branch). Migration: `20260622000000_chk235_task1_active_scope_per_branch.sql`.
|
|
113
|
+
|
|
112
114
|
## 8. Deployment — Railway
|
|
113
115
|
|
|
114
116
|
`apps/todo-worker` runs as a Railway service alongside `apps/backend`. Setup:
|
|
@@ -24,15 +24,15 @@ Precedence is `deny > ask > allow`; arrays union across scopes (managed/user/pro
|
|
|
24
24
|
|
|
25
25
|
- **Non-lifecycle, non-shipment `/cbp-*` skills** — authoring (`cbp-build-cc-*`), frontend (`cbp-frontend-*`), git (`cbp-git-*`, `cbp-merge-main`, `cbp-refresh-infra`), round work (`cbp-round-plan`, `cbp-verify` — `cbp-verify` is the autonomous verify stage that runs deterministic gates, proves execution, spawns the fresh-context reviewer, and routes to `cbp-round-complete` or `cbp-round-plan`, so it runs without a prompt), setup/configure (`cbp-setup-*`, `cbp-ship-configure`, `cbp-supabase-*`), task prep (`cbp-task-create`/`-start`, `cbp-standalone-task-check`/`-testing`), planning (`cbp-checkpoint-plan`/`-update`), plus `cbp-session-start` and `cbp-todo`. Invoking a skill is the intended mode of operation; the gated side effects happen inside via the Bash/MCP tools the skill calls, which carry their own tiering. The lifecycle/state-transition and plan-approval skills are the exception — they live in `ask` (next section).
|
|
26
26
|
- **All `mcp__codebyplan__*` reads** (`get_*`, `list_*`, `search_*`, `health_check`, `lookup_symbol`, `resolve_library_id`, `get_chunk`).
|
|
27
|
-
- **Routine workflow-write MCP tools** the pipeline calls many times per task: create/update/complete checkpoint, task, and round; session log + session-state writes; `
|
|
28
|
-
- **Read/safe CLI commands** (both `codebyplan X` and `npx codebyplan X`): `whoami`, `
|
|
27
|
+
- **Routine workflow-write MCP tools** the pipeline calls many times per task: create/update/complete checkpoint, task, and round; session log + session-state writes; `add_library`, `flag_stale_chunk`, `update_server_config`, `update_eslint_repo_config`. Gating these with `ask` would make the autonomous workflow unusable.
|
|
28
|
+
- **Read/safe CLI commands** (both `codebyplan X` and `npx codebyplan X`): `whoami`, `docs`, `statusline`, `ports`, `tech-stack`, `eslint`, `round`, `help`, `--version`.
|
|
29
29
|
|
|
30
30
|
### `ask` — the deliberate confirm-gate
|
|
31
31
|
|
|
32
32
|
- **Production-shipment skills**: `cbp-ship`, `cbp-ship-main`, `cbp-checkpoint-end` — these promote/deploy to production, so they prompt even in an otherwise auto-allowed setup.
|
|
33
33
|
- **Lifecycle / state-transition skills**: `cbp-checkpoint-start`, `cbp-checkpoint-create`, `cbp-checkpoint-check`, `cbp-checkpoint-complete`, `cbp-round-complete`, `cbp-session-end`, `cbp-finalize`, `cbp-standalone-task-create`, `cbp-standalone-task-start`, `cbp-standalone-task-complete` — these open or close checkpoints, tasks, rounds, and sessions (advancing workflow state in the database), so they stop for explicit confirmation rather than running autonomously. `cbp-round-complete` is the permission-gated round finalizer (reconciles the user's `git add`s, completes the round, routes onward); its `ask` prompt is the human gate downstream of `cbp-verify` — the autonomous, `allow`-tier verify stage whose triage routes here.
|
|
34
34
|
- **Plan-approval gate**: `cbp-round-build` — the round plan is approved by confirming this `ask` prompt rather than via an in-skill AskUserQuestion. `cbp-round-plan` runs its planning Q&A, then hands off to `cbp-round-build`; the permission prompt is the user's go/no-go on the plan.
|
|
35
|
-
- **Destructive / admin MCP tools**: `delete_session_log`, `
|
|
35
|
+
- **Destructive / admin MCP tools**: `delete_session_log`, `create_repo`, `release_assignment`. (The launch and member-admin tools were dropped from the MCP surface in CHK-180 — those concerns are web-app only now.)
|
|
36
36
|
- **Mutating / external / clobber-risk CLI commands** (both prefixes): `setup`, `login`, `logout`, `upgrade-auth`, `config` (can overwrite committed `.codebyplan/` files), `branch` (rewrites branch config), `ship`, `claude` (`install`/`update`/`uninstall` overwrite `.claude/`).
|
|
37
37
|
|
|
38
38
|
### `deny` — unchanged
|
|
@@ -280,15 +280,21 @@ Scan the output for any `worktree` entry whose `branch` field matches `refs/head
|
|
|
280
280
|
**Case A — the current session is NOT inside that worktree path:**
|
|
281
281
|
|
|
282
282
|
```bash
|
|
283
|
-
codebyplan worktree remove <path>
|
|
283
|
+
codebyplan worktree remove <abs-path> # pass the absolute path from worktree list
|
|
284
284
|
git worktree prune
|
|
285
285
|
```
|
|
286
286
|
|
|
287
|
-
|
|
288
|
-
|
|
287
|
+
`codebyplan worktree remove <abs-path>` handles both the git worktree removal and the
|
|
288
|
+
remote feat-branch deletion (non-fatal). No separate `git push origin --delete` is needed
|
|
289
|
+
here. Parse the JSON output; record `{ path, removed: true, remote_branch_deleted }` in
|
|
290
|
+
`WORKTREES_REMOVED[]`. On any non-zero exit (except exit code 2 — see Case B), emit a
|
|
291
|
+
non-blocking warning and continue — a failed removal does not halt shipment.
|
|
289
292
|
|
|
290
|
-
**Case B — the current session IS inside that worktree path
|
|
291
|
-
|
|
293
|
+
**Case B — the current session IS inside that worktree path:**
|
|
294
|
+
|
|
295
|
+
The CLI self-cwd guard fires and exits with **code 2** when `codebyplan worktree remove`
|
|
296
|
+
is called from inside the target worktree. Detect this via exit code 2, OR pre-check that
|
|
297
|
+
`$PWD` starts with `<path>` before calling the CLI.
|
|
292
298
|
|
|
293
299
|
Do NOT self-remove. Surface a single directive (no A/B/C menu):
|
|
294
300
|
|
|
@@ -169,7 +169,7 @@ Skip the push only when nothing was committed in Step 5 AND `/cbp-merge-main` re
|
|
|
169
169
|
|
|
170
170
|
### Step 7: Complete Task
|
|
171
171
|
|
|
172
|
-
MCP `complete_task(task_id)
|
|
172
|
+
MCP `complete_task(task_id)`. The server keys on the JWT user (`ctx.userId`) — no worktree param is needed. The server auto-clears `assigned_user_id` on the task; if this was the last sibling task, it also clears the parent checkpoint's assignment.
|
|
173
173
|
|
|
174
174
|
### Step 8: Run Cleanup + Migration (inline)
|
|
175
175
|
|
|
@@ -226,7 +226,7 @@ direct you to run `/cbp-clear-prep` first; otherwise checkpoint-check starts on
|
|
|
226
226
|
- **Triggered by**: `/cbp-verify` (auto, scope=task, when it writes `verify_verdict.verdict === 'READY'`)
|
|
227
227
|
- **Chain**: `/cbp-verify` (scope=task READY) → `/cbp-finalize`
|
|
228
228
|
- **Reads**: `.codebyplan/state/checkpoints/*.json`, `checkpoints/<id>/tasks/*.json`, `checkpoints/<id>/tasks/<id>/rounds/*.json`, `todos.json` (local-first; `npx codebyplan sync` on miss; MCP `get_current_task`/`get_rounds`/`get_tasks` break-glass) — including each round's `verify_manifest` and `task.context.verify_verdict`
|
|
229
|
-
- **Writes**: `codebyplan task update` for `files_changed` (CLI write-through; MCP `update_task` break-glass); MCP `complete_task` for task completion
|
|
229
|
+
- **Writes**: `codebyplan task update` for `files_changed` (CLI write-through; MCP `update_task` break-glass); MCP `complete_task` for task completion
|
|
230
230
|
- **Uses skills (inline, no sub-agent)**: `cleanup` (if deletions), `migration` (if exports renamed)
|
|
231
231
|
- **Triggers**: Same-context transitions auto-trigger via the Skill tool (next task in checkpoint → `cbp-task-start {N}`, `allow`-tier, fires silently). Checkpoint-done → auto-triggers `cbp-checkpoint-check` via Skill tool (`ask`-tier, permission prompt IS the human gate). No-task-anywhere fallback → directive `Next: Run /clear, then /cbp-session-end.`
|
|
232
232
|
- **Checkpoint-bound only** — for standalone tasks use `/cbp-standalone-task-complete`
|
|
@@ -249,6 +249,45 @@ When `branch_deleted === true` in the ship JSON:
|
|
|
249
249
|
- If the `list_branches` call itself fails (network, auth, or non-success response): emit a non-blocking warning that the Supabase preview branch for `FEAT_BRANCH` may still exist and should be verified in the dashboard. Never treat an API failure as a not-found success.
|
|
250
250
|
- Never delete the branch where `is_default` is true in the `list_branches` response (the production/parent project branch) or any other persistent/long-lived branch.
|
|
251
251
|
|
|
252
|
+
#### Step 7.4 — Git-worktree cleanup (defensive)
|
|
253
|
+
|
|
254
|
+
Read `FEAT_BRANCH` from the `feat_branch` field in the ship JSON (same source as Step 7.3).
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
git worktree list --porcelain
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Scan for any `worktree` entry whose `branch` field matches `refs/heads/$FEAT_BRANCH`. If NO
|
|
261
|
+
entry matches, skip this step silently — most standalone tasks have no dedicated worktree,
|
|
262
|
+
so a no-match is the normal case.
|
|
263
|
+
|
|
264
|
+
If a match exists, obtain its absolute path and apply the same Case A / Case B logic:
|
|
265
|
+
|
|
266
|
+
**Case A — session cwd is NOT inside that worktree path:**
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
codebyplan worktree remove <abs-path> # absolute path from worktree list
|
|
270
|
+
git worktree prune
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The CLI handles both the git removal and remote feat-branch deletion (non-fatal). Parse
|
|
274
|
+
the JSON output; record `{ path, removed: true, remote_branch_deleted }` in
|
|
275
|
+
`WORKTREES_REMOVED[]`. On any non-zero exit other than exit code 2, emit a non-blocking
|
|
276
|
+
warning and continue.
|
|
277
|
+
|
|
278
|
+
**Case B — session cwd IS inside that worktree path (CLI exits with code 2, or pre-check
|
|
279
|
+
detects `$PWD` starts with `<path>`):**
|
|
280
|
+
|
|
281
|
+
Do NOT self-remove. Surface a single directive:
|
|
282
|
+
|
|
283
|
+
> "This session is inside the worktree at `<path>`. After switching to your main checkout,
|
|
284
|
+
> run `codebyplan worktree remove <path>` to clean it up."
|
|
285
|
+
|
|
286
|
+
Record `{ path, status: 'pending_manual_cleanup' }` in `WORKTREES_REMOVED[]`.
|
|
287
|
+
|
|
288
|
+
In either case (and when no worktree matched — `WORKTREES_REMOVED` stays empty), include
|
|
289
|
+
`worktrees_removed: WORKTREES_REMOVED` in the Step 9 summary.
|
|
290
|
+
|
|
252
291
|
### Step 7.5: Complete Standalone Task
|
|
253
292
|
|
|
254
293
|
Note: completion is called only after `codebyplan ship` succeeds (no `checks_failed`) — the DB completion record reflects work that has landed in production.
|
|
@@ -280,6 +319,7 @@ Apply the `cleanup` skill inline to remove orphan references to deleted/modified
|
|
|
280
319
|
**Version bumps**: [<name>: <current> → <next> per package, or "none"]
|
|
281
320
|
**Export**: [EXPORT_RESULT.path from Step 5.6, or "skipped: <reason>"]
|
|
282
321
|
**Warnings**: [any QA / file-approval warnings from Step 3, or "none"]
|
|
322
|
+
**Git worktrees cleaned**: [paths from WORKTREES_REMOVED (Step 7.4), or "none"]
|
|
283
323
|
```
|
|
284
324
|
|
|
285
325
|
#### Route (single directive — never a menu)
|