codebyplan 1.10.1 → 1.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ var VERSION, PACKAGE_NAME;
|
|
|
14
14
|
var init_version = __esm({
|
|
15
15
|
"src/lib/version.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
VERSION = "1.10.
|
|
17
|
+
VERSION = "1.10.3";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -22,13 +22,16 @@ var init_version = __esm({
|
|
|
22
22
|
// src/lib/local-config.ts
|
|
23
23
|
import { execSync } from "node:child_process";
|
|
24
24
|
import { createHash } from "node:crypto";
|
|
25
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
25
|
+
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
26
26
|
import { hostname } from "node:os";
|
|
27
27
|
import { dirname, join } from "node:path";
|
|
28
28
|
function localConfigPath(projectPath) {
|
|
29
29
|
return join(projectPath, ".codebyplan", "device.local.json");
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
function legacyLocalConfigPath(projectPath) {
|
|
32
|
+
return join(projectPath, ".codebyplan.local.json");
|
|
33
|
+
}
|
|
34
|
+
async function readLocalConfig(projectPath, onMigrationNotice) {
|
|
32
35
|
try {
|
|
33
36
|
const raw = await readFile(localConfigPath(projectPath), "utf-8");
|
|
34
37
|
const parsed = JSON.parse(raw);
|
|
@@ -38,22 +41,81 @@ async function readLocalConfig(projectPath) {
|
|
|
38
41
|
console.error("Failed to read local config: invalid shape");
|
|
39
42
|
return null;
|
|
40
43
|
} catch (err) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
)
|
|
44
|
+
const code = err.code;
|
|
45
|
+
if (code === "ENOENT") {
|
|
46
|
+
} else if (code === "ENOTDIR") {
|
|
47
|
+
try {
|
|
48
|
+
const dirPath = dirname(localConfigPath(projectPath));
|
|
49
|
+
const st = await stat(dirPath);
|
|
50
|
+
if (!st.isDirectory()) {
|
|
51
|
+
throw Object.assign(
|
|
52
|
+
new Error(`${dirPath} is a file, expected directory`),
|
|
53
|
+
{ code: "LEGACY_FILE_BLOCKS_DIR" }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
} catch (statErr) {
|
|
57
|
+
if (statErr.code === "LEGACY_FILE_BLOCKS_DIR")
|
|
58
|
+
throw statErr;
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
} else if (typeof code === "string") {
|
|
62
|
+
throw err;
|
|
63
|
+
} else {
|
|
64
|
+
console.error(
|
|
65
|
+
`Failed to read local config: ${err instanceof Error ? err.message : String(err)}`
|
|
66
|
+
);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const raw = await readFile(legacyLocalConfigPath(projectPath), "utf-8");
|
|
72
|
+
const parsed = JSON.parse(raw);
|
|
73
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
|
|
74
|
+
onMigrationNotice?.(
|
|
75
|
+
legacyLocalConfigPath(projectPath),
|
|
76
|
+
localConfigPath(projectPath)
|
|
77
|
+
);
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
44
80
|
return null;
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const code = err.code;
|
|
83
|
+
if (code === "ENOENT") {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
45
87
|
}
|
|
46
88
|
}
|
|
47
89
|
async function writeLocalConfig(projectPath, config) {
|
|
48
90
|
const content = { device_id: config.device_id };
|
|
49
91
|
const path6 = localConfigPath(projectPath);
|
|
92
|
+
const dirPath = dirname(path6);
|
|
93
|
+
let phase = "stat config directory";
|
|
50
94
|
try {
|
|
51
|
-
|
|
95
|
+
try {
|
|
96
|
+
const st = await stat(dirPath);
|
|
97
|
+
if (!st.isDirectory()) {
|
|
98
|
+
const err = Object.assign(
|
|
99
|
+
new Error(`${dirPath} is a file, expected directory`),
|
|
100
|
+
{ code: "LEGACY_FILE_BLOCKS_DIR" }
|
|
101
|
+
);
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
} catch (statErr) {
|
|
105
|
+
const code = statErr.code;
|
|
106
|
+
if (code === "LEGACY_FILE_BLOCKS_DIR") throw statErr;
|
|
107
|
+
if (code !== "ENOENT") throw statErr;
|
|
108
|
+
}
|
|
109
|
+
phase = "create config directory";
|
|
110
|
+
await mkdir(dirPath, { recursive: true });
|
|
111
|
+
phase = "write local config";
|
|
52
112
|
await writeFile(path6, JSON.stringify(content, null, 2) + "\n", "utf-8");
|
|
53
113
|
} catch (err) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
114
|
+
const code = err.code;
|
|
115
|
+
if (code === "LEGACY_FILE_BLOCKS_DIR") {
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
console.error(`Failed to ${phase}: ${err.message}`);
|
|
57
119
|
throw err;
|
|
58
120
|
}
|
|
59
121
|
}
|
|
@@ -73,8 +135,8 @@ async function resolveMachineSeed() {
|
|
|
73
135
|
}
|
|
74
136
|
return hostname();
|
|
75
137
|
}
|
|
76
|
-
async function getOrCreateDeviceId(projectPath) {
|
|
77
|
-
const existing = await readLocalConfig(projectPath);
|
|
138
|
+
async function getOrCreateDeviceId(projectPath, onMigrationNotice) {
|
|
139
|
+
const existing = await readLocalConfig(projectPath, onMigrationNotice);
|
|
78
140
|
if (existing?.device_id) {
|
|
79
141
|
return existing.device_id;
|
|
80
142
|
}
|
|
@@ -516,8 +578,8 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
|
|
|
516
578
|
});
|
|
517
579
|
if (match) worktreeId = match.id;
|
|
518
580
|
} catch (err) {
|
|
519
|
-
console.
|
|
520
|
-
`Worktree lookup failed: ${err instanceof Error ? err.message : String(err)}
|
|
581
|
+
console.warn(
|
|
582
|
+
`Worktree self-heal skipped (non-fatal): worktree lookup failed: ${err instanceof Error ? err.message : String(err)}. Continuing.`
|
|
521
583
|
);
|
|
522
584
|
return void 0;
|
|
523
585
|
}
|
|
@@ -561,7 +623,8 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
|
|
|
561
623
|
async function resolveWorktreeByBranch({
|
|
562
624
|
repoId,
|
|
563
625
|
deviceId,
|
|
564
|
-
branch
|
|
626
|
+
branch,
|
|
627
|
+
onError
|
|
565
628
|
}) {
|
|
566
629
|
if (!deviceId || !branch) {
|
|
567
630
|
return null;
|
|
@@ -585,8 +648,12 @@ async function resolveWorktreeByBranch({
|
|
|
585
648
|
}
|
|
586
649
|
return match?.id ?? null;
|
|
587
650
|
} catch (err) {
|
|
588
|
-
console.
|
|
589
|
-
`
|
|
651
|
+
console.warn(
|
|
652
|
+
`Worktree self-heal skipped (non-fatal): branch worktree resolve failed: ${err instanceof Error ? err.message : String(err)}. Continuing.`
|
|
653
|
+
);
|
|
654
|
+
onError?.(
|
|
655
|
+
"api_failed",
|
|
656
|
+
err instanceof Error ? err : new Error(String(err))
|
|
590
657
|
);
|
|
591
658
|
return null;
|
|
592
659
|
}
|
|
@@ -595,7 +662,8 @@ async function resolveWorktreeId({
|
|
|
595
662
|
repoId,
|
|
596
663
|
repoPath,
|
|
597
664
|
branch,
|
|
598
|
-
deviceId
|
|
665
|
+
deviceId,
|
|
666
|
+
onError
|
|
599
667
|
}) {
|
|
600
668
|
try {
|
|
601
669
|
const res = await apiPost(
|
|
@@ -604,8 +672,12 @@ async function resolveWorktreeId({
|
|
|
604
672
|
);
|
|
605
673
|
return res.worktree_id ?? null;
|
|
606
674
|
} catch (err) {
|
|
607
|
-
console.
|
|
608
|
-
`
|
|
675
|
+
console.warn(
|
|
676
|
+
`Worktree self-heal skipped (non-fatal): ${err instanceof Error ? err.message : String(err)}. Continuing \u2014 run \`codebyplan config\` again later to retry.`
|
|
677
|
+
);
|
|
678
|
+
onError?.(
|
|
679
|
+
"api_failed",
|
|
680
|
+
err instanceof Error ? err : new Error(String(err))
|
|
609
681
|
);
|
|
610
682
|
return null;
|
|
611
683
|
}
|
|
@@ -3806,63 +3878,145 @@ var init_ship2 = __esm({
|
|
|
3806
3878
|
// src/cli/resolve-worktree.ts
|
|
3807
3879
|
var resolve_worktree_exports = {};
|
|
3808
3880
|
__export(resolve_worktree_exports, {
|
|
3881
|
+
ProcessExitSignal: () => ProcessExitSignal,
|
|
3809
3882
|
runResolveWorktree: () => runResolveWorktree
|
|
3810
3883
|
});
|
|
3811
3884
|
import { execSync as execSync5 } from "node:child_process";
|
|
3885
|
+
function distress(kind, message, jsonMode) {
|
|
3886
|
+
if (jsonMode) return;
|
|
3887
|
+
process.stderr.write(`resolve-worktree: ${kind}: ${message}
|
|
3888
|
+
`);
|
|
3889
|
+
}
|
|
3812
3890
|
async function runResolveWorktree() {
|
|
3891
|
+
const jsonMode = hasFlag("json", 3);
|
|
3892
|
+
let errorContext = null;
|
|
3893
|
+
const migrationNoticeCallback = (legacyPath, primaryPath) => {
|
|
3894
|
+
if (!jsonMode) {
|
|
3895
|
+
process.stderr.write(
|
|
3896
|
+
`resolve-worktree: local_config_migration: ${legacyPath} is the legacy flat config; move device_id to ${primaryPath}
|
|
3897
|
+
`
|
|
3898
|
+
);
|
|
3899
|
+
}
|
|
3900
|
+
};
|
|
3813
3901
|
try {
|
|
3814
3902
|
const projectPath = process.cwd();
|
|
3815
3903
|
const found = await findCodebyplanConfig(projectPath);
|
|
3816
3904
|
if (!found?.contents.repo_id) {
|
|
3817
|
-
|
|
3905
|
+
emitAndExit(null, null, jsonMode);
|
|
3818
3906
|
}
|
|
3819
3907
|
const repoId = found.contents.repo_id;
|
|
3820
|
-
|
|
3908
|
+
try {
|
|
3909
|
+
await readLocalConfig(projectPath);
|
|
3910
|
+
} catch (readErr) {
|
|
3911
|
+
const readErrCode = readErr.code;
|
|
3912
|
+
errorContext = {
|
|
3913
|
+
kind: readErrCode === "LEGACY_FILE_BLOCKS_DIR" ? "legacy_file_blocks_dir" : "local_config_read_failed",
|
|
3914
|
+
message: readErr instanceof Error ? readErr.message : String(readErr)
|
|
3915
|
+
};
|
|
3916
|
+
}
|
|
3917
|
+
let deviceId;
|
|
3918
|
+
try {
|
|
3919
|
+
deviceId = await getOrCreateDeviceId(
|
|
3920
|
+
projectPath,
|
|
3921
|
+
migrationNoticeCallback
|
|
3922
|
+
);
|
|
3923
|
+
} catch (deviceErr) {
|
|
3924
|
+
const code = deviceErr.code;
|
|
3925
|
+
if (code === "LEGACY_FILE_BLOCKS_DIR") {
|
|
3926
|
+
errorContext = {
|
|
3927
|
+
kind: "legacy_file_blocks_dir",
|
|
3928
|
+
message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
|
|
3929
|
+
};
|
|
3930
|
+
} else if (errorContext === null || errorContext.kind !== "local_config_read_failed" && errorContext.kind !== "legacy_file_blocks_dir") {
|
|
3931
|
+
errorContext = {
|
|
3932
|
+
kind: "local_config_write_failed",
|
|
3933
|
+
message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
|
|
3934
|
+
};
|
|
3935
|
+
}
|
|
3936
|
+
emitAndExit(null, errorContext, jsonMode);
|
|
3937
|
+
}
|
|
3821
3938
|
let branch = "";
|
|
3822
3939
|
try {
|
|
3823
3940
|
branch = execSync5("git symbolic-ref --short HEAD", {
|
|
3824
3941
|
cwd: projectPath,
|
|
3825
3942
|
encoding: "utf-8"
|
|
3826
3943
|
}).trim();
|
|
3827
|
-
} catch {
|
|
3944
|
+
} catch (gitErr) {
|
|
3945
|
+
if (errorContext === null) {
|
|
3946
|
+
errorContext = {
|
|
3947
|
+
kind: "git_failed",
|
|
3948
|
+
message: gitErr instanceof Error ? gitErr.message : String(gitErr)
|
|
3949
|
+
};
|
|
3950
|
+
}
|
|
3828
3951
|
}
|
|
3952
|
+
const onResolverError = (kind, err) => {
|
|
3953
|
+
if (errorContext === null) {
|
|
3954
|
+
errorContext = { kind, message: err.message };
|
|
3955
|
+
}
|
|
3956
|
+
};
|
|
3829
3957
|
const worktreeId = await resolveWorktreeId({
|
|
3830
3958
|
repoId,
|
|
3831
3959
|
repoPath: projectPath,
|
|
3832
3960
|
branch,
|
|
3833
|
-
deviceId
|
|
3961
|
+
deviceId,
|
|
3962
|
+
onError: onResolverError
|
|
3834
3963
|
});
|
|
3835
3964
|
if (worktreeId) {
|
|
3836
|
-
|
|
3837
|
-
process.exit(0);
|
|
3965
|
+
emitAndExit(worktreeId, errorContext, jsonMode);
|
|
3838
3966
|
}
|
|
3839
3967
|
const useFallback = hasFlag("fallback-from-branch", 3);
|
|
3840
3968
|
if (useFallback) {
|
|
3841
3969
|
const fallbackId = await resolveWorktreeByBranch({
|
|
3842
3970
|
repoId,
|
|
3843
3971
|
deviceId,
|
|
3844
|
-
branch
|
|
3972
|
+
branch,
|
|
3973
|
+
onError: onResolverError
|
|
3845
3974
|
});
|
|
3846
3975
|
if (fallbackId) {
|
|
3847
|
-
|
|
3976
|
+
emitAndExit(fallbackId, errorContext, jsonMode);
|
|
3848
3977
|
}
|
|
3849
3978
|
}
|
|
3850
|
-
|
|
3979
|
+
emitAndExit(null, errorContext, jsonMode);
|
|
3851
3980
|
} catch (err) {
|
|
3852
|
-
if (
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3981
|
+
if (err instanceof ProcessExitSignal) throw err;
|
|
3982
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3983
|
+
errorContext = { kind: "unhandled", message: msg };
|
|
3984
|
+
emitAndExit(null, errorContext, jsonMode);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
function emitAndExit(worktreeId, errorContext, jsonMode) {
|
|
3988
|
+
if (jsonMode) {
|
|
3989
|
+
const errorKind = errorContext?.kind ?? (worktreeId === null ? "tuple_miss" : null);
|
|
3990
|
+
process.stdout.write(
|
|
3991
|
+
JSON.stringify({ worktree_id: worktreeId, error_kind: errorKind }) + "\n"
|
|
3992
|
+
);
|
|
3993
|
+
} else {
|
|
3994
|
+
if (worktreeId !== null) {
|
|
3995
|
+
process.stdout.write(worktreeId);
|
|
3996
|
+
}
|
|
3997
|
+
if (errorContext !== null) {
|
|
3998
|
+
if (errorContext.kind !== "unhandled" || process.env.CODEBYPLAN_DEBUG === "1") {
|
|
3999
|
+
distress(errorContext.kind, errorContext.message, jsonMode);
|
|
4000
|
+
}
|
|
3856
4001
|
}
|
|
3857
|
-
process.exit(0);
|
|
3858
4002
|
}
|
|
4003
|
+
process.exit(0);
|
|
3859
4004
|
}
|
|
4005
|
+
var ProcessExitSignal;
|
|
3860
4006
|
var init_resolve_worktree2 = __esm({
|
|
3861
4007
|
"src/cli/resolve-worktree.ts"() {
|
|
3862
4008
|
"use strict";
|
|
3863
4009
|
init_flags();
|
|
3864
4010
|
init_local_config();
|
|
3865
4011
|
init_resolve_worktree();
|
|
4012
|
+
ProcessExitSignal = class extends Error {
|
|
4013
|
+
code;
|
|
4014
|
+
constructor(code) {
|
|
4015
|
+
super(`process.exit(${code})`);
|
|
4016
|
+
this.name = "ProcessExitSignal";
|
|
4017
|
+
this.code = code;
|
|
4018
|
+
}
|
|
4019
|
+
};
|
|
3866
4020
|
}
|
|
3867
4021
|
});
|
|
3868
4022
|
|
|
@@ -3882,9 +4036,9 @@ function sentinelPath(projectPath) {
|
|
|
3882
4036
|
return join12(projectPath, ".codebyplan", "repo.json");
|
|
3883
4037
|
}
|
|
3884
4038
|
async function statSafe(p) {
|
|
3885
|
-
const { stat } = await import("node:fs/promises");
|
|
4039
|
+
const { stat: stat2 } = await import("node:fs/promises");
|
|
3886
4040
|
try {
|
|
3887
|
-
return await
|
|
4041
|
+
return await stat2(p);
|
|
3888
4042
|
} catch {
|
|
3889
4043
|
return null;
|
|
3890
4044
|
}
|
|
@@ -4153,7 +4307,27 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
4153
4307
|
` Warning: failed to cache worktree_id (self-heal skipped): ${msg}`
|
|
4154
4308
|
);
|
|
4155
4309
|
}
|
|
4156
|
-
|
|
4310
|
+
let repoRes;
|
|
4311
|
+
try {
|
|
4312
|
+
repoRes = await apiGet(`/repos/${repoId}`);
|
|
4313
|
+
} catch (err) {
|
|
4314
|
+
let message;
|
|
4315
|
+
if (err instanceof ApiError) {
|
|
4316
|
+
if (err.status === 401) {
|
|
4317
|
+
message = "Session expired. Run `codebyplan login` and try again.";
|
|
4318
|
+
} else if (err.status === 403 || err.status === 404) {
|
|
4319
|
+
message = "Repo not found or not accessible to your account. Confirm the repo_id in .codebyplan/repo.json and that you are logged in as the right user.";
|
|
4320
|
+
} else if (err.status >= 500) {
|
|
4321
|
+
message = `CodeByPlan server error (status ${err.status}). Please try again shortly.`;
|
|
4322
|
+
} else {
|
|
4323
|
+
message = `Unexpected API error (status ${err.status}). Please try again.`;
|
|
4324
|
+
}
|
|
4325
|
+
} else {
|
|
4326
|
+
message = "Failed to reach the CodeByPlan API. Check your network connection and try again.";
|
|
4327
|
+
}
|
|
4328
|
+
process.stderr.write(message + "\n");
|
|
4329
|
+
process.exit(1);
|
|
4330
|
+
}
|
|
4157
4331
|
const repo = repoRes.data;
|
|
4158
4332
|
let portAllocations = [];
|
|
4159
4333
|
try {
|
|
@@ -5990,8 +6164,8 @@ function pruneEmptyManagedDirs(projectDir) {
|
|
|
5990
6164
|
}
|
|
5991
6165
|
function pruneLeafFirst(dir) {
|
|
5992
6166
|
if (!fs5.existsSync(dir)) return;
|
|
5993
|
-
const
|
|
5994
|
-
if (!
|
|
6167
|
+
const stat2 = fs5.statSync(dir);
|
|
6168
|
+
if (!stat2.isDirectory()) return;
|
|
5995
6169
|
for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
|
|
5996
6170
|
if (entry.isDirectory()) {
|
|
5997
6171
|
pruneLeafFirst(path5.join(dir, entry.name));
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ Activate the session, open a fresh session log, and surface the previous log's p
|
|
|
12
12
|
|
|
13
13
|
## Instructions
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Run Steps 0 through 5.8 silently (no intermediate output) — except Step 1.4 may surface a one-line fast-forward note or warning, and Step 5.7 may surface an approval gate. (Step numbers are organizational labels; execution order is 0 → 1 → 1.4 → 2 → 3 → 4 → 4.5 → 5 → 5.7 → 5.8 → 6 → 7.) Produce ONE output block at Step 6, then auto-trigger or stop per Step 7.
|
|
16
16
|
|
|
17
17
|
### Step 0: MCP Health Check
|
|
18
18
|
|
|
@@ -40,15 +40,19 @@ Read per-concern config files from the project root. Single load point for the s
|
|
|
40
40
|
- `server_port`, `server_type`, `auto_push_enabled` — from `.codebyplan/server.json`
|
|
41
41
|
- `git_branch` — from `.codebyplan/git.json`
|
|
42
42
|
|
|
43
|
-
Resolve `worktree_id` at runtime
|
|
43
|
+
Resolve `worktree_id` at runtime using the structured JSON form:
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
|
-
|
|
46
|
+
RESOLVE_JSON=$(npx codebyplan resolve-worktree --json)
|
|
47
|
+
# → {"worktree_id":"<uuid>|null","error_kind":null|"<kind>"}
|
|
47
48
|
```
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
Extract `worktree_id` and `error_kind` from the JSON output.
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
- `error_kind` is `null` or `"tuple_miss"` → healthy. `WORKTREE_ID` = `worktree_id` (may be `null`: a legitimate main-repo or unregistered-worktree case — proceed normally; downstream hard-lock pre-guards may reject mutations on assigned rows).
|
|
53
|
+
- `error_kind` is `local_config_read_failed`, `local_config_write_failed`, `legacy_file_blocks_dir`, `api_failed`, `git_failed`, or `unhandled` → **broken local state**. Hold the `error_kind` for Step 6 to display as a distress warning. Session continues (non-blocking — unlike `/cbp-todo`, session-start does NOT hard-stop on a non-tuple-miss distress).
|
|
54
|
+
|
|
55
|
+
Pass `WORKTREE_ID` to MCP tools that support it. Null `WORKTREE_ID` means the (device, path, branch) tuple is unregistered — note this for Step 6.
|
|
52
56
|
|
|
53
57
|
### Step 1.4: Home-Branch Fast-Forward
|
|
54
58
|
|
|
@@ -123,6 +127,7 @@ Probe the most-recent closed session log for a structured handoff payload (per `
|
|
|
123
127
|
- `round_id` → `get_rounds({ task_id: handoff.context.task_id })` → find entry where `entry.id === handoff.context.round_id`
|
|
124
128
|
Then compare `entry.updated_at > handoff.captured_at` → stale on any inequality.
|
|
125
129
|
- Entity lookup fails OR the matching `id` is not present in the returned array → stale (referenced entity gone or moved out of reach).
|
|
130
|
+
- `handoff.context.checkpoint_id` resolves to a checkpoint whose `worktree_id` is non-null AND (caller `WORKTREE_ID` is `null` OR differs from `checkpoint.worktree_id`) → stale (a fresh handoff for another worktree's work — or for assigned work this caller cannot confirm ownership of — must not auto-resume here). Mirrors the cbp-todo Step 1.5 ownership rule.
|
|
126
131
|
4. **On stale OR any defensive gate hit**: fall through silently to Step 7 (existing `/cbp-todo` trigger).
|
|
127
132
|
5. **On fresh hit**: trigger `handoff.command` directly with `handoff.context` / `handoff.state` in the trigger arguments. The downstream skill self-loads its full context — do NOT duplicate `/cbp-todo` Step 2's context-loading matrix here. Skip Step 5.7, Step 6 output, and Step 7.
|
|
128
133
|
|
|
@@ -152,27 +157,50 @@ Clean the working tree of leftover infra before the session begins. Only commit
|
|
|
152
157
|
|
|
153
158
|
Non-blocking — session start proceeds either way.
|
|
154
159
|
|
|
160
|
+
### Step 5.8: Resolve Ownership
|
|
161
|
+
|
|
162
|
+
Call MCP `get_checkpoints({ repo_id, status: 'active' })`. Partition results into:
|
|
163
|
+
|
|
164
|
+
- `owned[]` — entries where `checkpoint.worktree_id === WORKTREE_ID`, OR both are `null`
|
|
165
|
+
- `cross_worktree[]` — entries where `checkpoint.worktree_id` is non-null AND differs from `WORKTREE_ID` (includes the case where caller `WORKTREE_ID` is `null` but the target has a non-null `worktree_id`)
|
|
166
|
+
|
|
167
|
+
Hold `owned_count = owned.length`, `total_count = owned.length + cross_worktree.length`, `owned_names` (CHK-NNN + title for each owned entry), and `cross_names` (CHK-NNN + name for each cross-worktree entry). These values are consumed by Step 6 and Step 7 — single MCP call, no duplicate round-trips.
|
|
168
|
+
|
|
155
169
|
### Step 6: Output
|
|
156
170
|
|
|
157
171
|
```
|
|
158
|
-
Session active | Worktree: [worktree_id or "
|
|
172
|
+
Session active | Worktree: [worktree_id or "unregistered"]
|
|
173
|
+
|
|
174
|
+
[⚠ resolve-worktree: <error_kind> — local state is broken; routing may be unreliable. Run `npx codebyplan setup` to repair. — only when error_kind is non-null and not tuple_miss]
|
|
159
175
|
|
|
160
176
|
Previous session: [title or "none"]
|
|
161
177
|
Pending: [pending items from previous log, or "—"]
|
|
162
178
|
|
|
179
|
+
Ownership: [total_count] active CHK(s), [owned_count] owned by this worktree
|
|
180
|
+
[Owned: CHK-NNN (title), … — only when owned_count > 0]
|
|
181
|
+
[Cross-worktree: CHK-ZZZ (name), … — only when total_count > owned_count]
|
|
182
|
+
|
|
163
183
|
[⚠ Dev server not running — start via desktop app — only if applicable]
|
|
184
|
+
[⚠ Worktree unregistered — run `npx codebyplan setup` to register — only when WORKTREE_ID is null and no resolver distress was already shown]
|
|
164
185
|
```
|
|
165
186
|
|
|
166
|
-
|
|
187
|
+
READ-ONLY — this block never proposes reassignment, release, or lock transfer of cross-worktree checkpoints.
|
|
188
|
+
|
|
189
|
+
### Step 7: Auto-trigger
|
|
190
|
+
|
|
191
|
+
Three-branch gate using `owned_count` and `total_count` from Step 5.8:
|
|
167
192
|
|
|
168
|
-
|
|
193
|
+
- **`owned_count >= 1`**: trigger `/cbp-todo` (owns active work — proceed as today).
|
|
194
|
+
- **`total_count >= 1` AND `owned_count === 0`**: **STOP** — do NOT auto-trigger `/cbp-todo`. The Ownership block shown in Step 6 already communicates the situation; the user must switch to the owning worktree or start new work explicitly.
|
|
195
|
+
- **`total_count === 0`** (no active checkpoints anywhere): trigger `/cbp-todo` (idle path — leads to checkpoint-create or session-end).
|
|
169
196
|
|
|
170
197
|
## Integration
|
|
171
198
|
|
|
172
199
|
- **Triggered by**: user invocation, `/clear` recovery
|
|
173
|
-
- **
|
|
200
|
+
- **Resolves**: `npx codebyplan resolve-worktree --json` (worktree id + distress signal; non-tuple-miss distress is non-blocking at session-start)
|
|
201
|
+
- **Reads**: `.codebyplan/repo.json`, `.codebyplan/git.json` (`branch_config.production` for the Step 1.4 home-branch fast-forward), MCP `get_session_logs` (worktree-filtered, limit 1 — single call shared by Step 4 and Step 4.5), MCP `health_check`, MCP `get_current_task`, MCP `get_rounds`, MCP `get_checkpoints` (two calls: `{ repo_id, status: 'active' }` for the Step 5.8 ownership partition; `{ repo_id }` unfiltered for the Step 4.5 freshness probe, which may resolve a non-active checkpoint), MCP `get_tasks` / `get_rounds` for the Step 4.5 freshness probe
|
|
174
202
|
- **Writes**: MCP `create_session_log` (new, possibly empty), MCP `update_session_state` (activate)
|
|
175
203
|
- **Spawns**: none
|
|
176
|
-
- **Triggers**: `/cbp-git-commit` (conditional, on user approval), `handoff.command` (on fresh handoff hit at Step 4.5), `/cbp-todo` (auto fall-through)
|
|
204
|
+
- **Triggers**: `/cbp-git-commit` (conditional, on user approval), `handoff.command` (on fresh handoff hit at Step 4.5), `/cbp-todo` (auto fall-through when owned_count >= 1 or total_count === 0; STOPS with no trigger when total_count >= 1 AND owned_count === 0)
|
|
177
205
|
- **Paired with**: `/cbp-session-end`
|
|
178
206
|
- **Pairs with**: `.claude/rules/session-resume.md` (handoff payload shape + freshness gate contract)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
scope: org-shared
|
|
3
|
+
name: cbp-session-start-qa-regression
|
|
4
|
+
description: Manual regression procedure for the cbp-session-start worktree-ownership awareness + resolve-worktree distress channel (CHK-137 TASK-3)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# cbp-session-start — Worktree-Ownership Regression
|
|
8
|
+
|
|
9
|
+
Manual procedure verifying that `/cbp-session-start` correctly resolves the caller's worktree identity, gates Step 7 auto-trigger on ownership, and surfaces distress signals non-blocking. No automated harness exists for markdown skills; run these by hand (or exercise the MCP calls directly) whenever Step 1, Step 4.5, Step 5.8, Step 6, or Step 7 of `SKILL.md` changes.
|
|
10
|
+
|
|
11
|
+
Repo under test: `2ff6d405-39c5-47b8-a6d1-59f998ac0537`.
|
|
12
|
+
|
|
13
|
+
## Preconditions
|
|
14
|
+
|
|
15
|
+
- Step 1 uses `resolve-worktree --json` (not the legacy `2>/dev/null` form) — confirm with `grep -n 'resolve-worktree' SKILL.md` → line contains `--json`.
|
|
16
|
+
- Step 5.8 calls `get_checkpoints({ repo_id, status: 'active' })` — confirm no Step 7 auto-trigger bypasses this gate.
|
|
17
|
+
- Step 6 Ownership block is READ-ONLY — confirm no "reassign", "release_assignment", or "transfer" language appears in SKILL.md: `grep -n 'reassign\|release_assignment\|transfer' SKILL.md` → no hits.
|
|
18
|
+
- Step 4.5 freshness gate includes the cross-worktree stale bullet (checkpoint `worktree_id` non-null and differs from caller).
|
|
19
|
+
|
|
20
|
+
## Scenario A — caller owns an active CHK → auto-trigger
|
|
21
|
+
|
|
22
|
+
1. Run from a worktree whose `WORKTREE_ID` matches the active checkpoint's `worktree_id` (or both are `null`).
|
|
23
|
+
2. `get_checkpoints({ repo_id, status: 'active' })` returns at least one entry whose `worktree_id === WORKTREE_ID` (or both null).
|
|
24
|
+
3. **Expected**: Step 5.8 sets `owned_count >= 1`. Step 6 shows `Ownership: N active CHK(s), N owned by this worktree`. Step 7 first branch fires: `/cbp-todo` is auto-triggered.
|
|
25
|
+
|
|
26
|
+
## Scenario B — active CHK(s) exist but none owned by caller → Ownership block + STOP
|
|
27
|
+
|
|
28
|
+
Repro: caller worktree is `codebyplan-claude-2` (`38cd7dfa`). The only active checkpoint is CHK-136, assigned to `codebyplan-cli` (`016bd7f2`).
|
|
29
|
+
|
|
30
|
+
1. `resolve-worktree --json` returns `{"worktree_id":"38cd7dfa-...","error_kind":null}`.
|
|
31
|
+
2. `get_checkpoints({ repo_id, status: 'active' })` returns CHK-136 with `worktree_id = "016bd7f2-..."`.
|
|
32
|
+
3. Step 5.8: `owned_count = 0`, `total_count = 1`, `cross_worktree = [CHK-136]`.
|
|
33
|
+
4. **Expected**: Step 6 shows `Ownership: 1 active CHK(s), 0 owned by this worktree` and `[Cross-worktree: CHK-136 (…)]`. Step 7 second branch fires: Ownership block is displayed (already in Step 6) and skill **STOPS** — `/cbp-todo` is NOT auto-triggered. No reassignment language appears.
|
|
34
|
+
|
|
35
|
+
## Scenario C — no active CHKs anywhere → idle /cbp-todo trigger
|
|
36
|
+
|
|
37
|
+
1. `get_checkpoints({ repo_id, status: 'active' })` returns `[]`.
|
|
38
|
+
2. Step 5.8: `owned_count = 0`, `total_count = 0`.
|
|
39
|
+
3. **Expected**: Step 6 shows `Ownership: 0 active CHK(s), 0 owned by this worktree`. Step 7 third branch fires: `/cbp-todo` is auto-triggered (idle path → checkpoint-create or session-end suggestion).
|
|
40
|
+
|
|
41
|
+
## Scenario D — cross-worktree handoff → Step 4.5 marks stale, falls through
|
|
42
|
+
|
|
43
|
+
1. The most-recent closed session log contains a handoff payload whose `context.checkpoint_id` resolves to a checkpoint with `worktree_id = "016bd7f2-..."` (a different worktree).
|
|
44
|
+
2. Caller `WORKTREE_ID = "38cd7dfa-..."`.
|
|
45
|
+
3. **Expected**: Step 4.5 freshness gate hits the cross-worktree stale bullet (`checkpoint.worktree_id` non-null AND differs from caller) → marks stale → falls through silently to Step 7. The mismatched handoff is NOT auto-resumed. Ownership output from Step 5.8 / Step 6 / Step 7 proceeds normally.
|
|
46
|
+
|
|
47
|
+
## Scenario E — resolver distress (non-tuple-miss) → warning line above Ownership block, session proceeds
|
|
48
|
+
|
|
49
|
+
1. `resolve-worktree --json` returns `{"worktree_id":null,"error_kind":"local_config_read_failed"}` (or any other non-null, non-tuple-miss `error_kind`).
|
|
50
|
+
2. **Expected**: Step 1 holds the `error_kind`; session continues (non-blocking). Step 6 surfaces `⚠ resolve-worktree: local_config_read_failed — local state is broken; routing may be unreliable. Run \`npx codebyplan setup\` to repair.` ABOVE the Ownership block. All subsequent steps still run (Steps 2–5.8). Step 7 proceeds per the `owned_count`/`total_count` values from Step 5.8 as normal — the skill does NOT hard-stop the way `/cbp-todo` does on the same distress kinds.
|
|
51
|
+
3. **Compound case** (distress typically leaves `WORKTREE_ID` null): Step 5.8 then classifies every checkpoint with a non-null `worktree_id` as `cross_worktree[]` (only truly-null-`worktree_id` checkpoints land in `owned[]`). So if all active checkpoints are assigned, `owned_count = 0` and Step 7's second branch STOPS — the distress warning + Ownership block are the only output. The session log created in Step 5 records `worktree_id: null` (the resolver could not read local state); this is expected, not a failure.
|
|
@@ -7,47 +7,95 @@ effort: low
|
|
|
7
7
|
|
|
8
8
|
# Todo Command
|
|
9
9
|
|
|
10
|
-
Single
|
|
10
|
+
Single entry point after `/clear`: read the pure-read todo queue, gate on worktree ownership + checkpoint planning, load context, then trigger the next command. It ensures Claude always has full context — and never auto-routes into work locked to another worktree — before any command runs.
|
|
11
11
|
|
|
12
12
|
## Instructions
|
|
13
13
|
|
|
14
|
-
### Step 0: Resolve Caller
|
|
14
|
+
### Step 0: Resolve Caller Identity (worktree + user)
|
|
15
15
|
|
|
16
|
-
Before any MCP call,
|
|
16
|
+
Before any MCP call, resolve who and where the caller is.
|
|
17
|
+
|
|
18
|
+
**Worktree** — derive the caller `worktree_id` and any distress signal in one structured read:
|
|
17
19
|
|
|
18
20
|
```bash
|
|
19
|
-
|
|
21
|
+
npx codebyplan resolve-worktree --json # → {"worktree_id":"<uuid>|null","error_kind":null|"<kind>"}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- `error_kind` is `null` or `"tuple_miss"` → healthy. `WORKTREE_ID` = `worktree_id` (may be `null`: a legitimate main-repo or unregistered-worktree case — proceed to read the queue, but downstream hard-locks reject mutations on assigned rows).
|
|
25
|
+
- `error_kind` is `local_config_read_failed`, `local_config_write_failed`, `legacy_file_blocks_dir`, `api_failed`, `git_failed`, or `unhandled` → **broken local state**. Surface the distress line and STOP (skip Steps 1–4) — routing is unreliable when the resolver cannot read local state:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
⚠ resolve-worktree: <error_kind> — local state is broken; routing is unreliable.
|
|
29
|
+
Run `npx codebyplan setup` from this directory to register/repair, then re-run /cbp-todo.
|
|
20
30
|
```
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
**User** — `get_todos` (Step 1) requires a `user_id`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx codebyplan whoami --json # → {"user_id":"<uuid>","email":"…"} or null
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- A `user_id` is returned → use it in Step 1.
|
|
39
|
+
- `null` (CLI keychain empty — common in OAuth-MCP-only environments) → the queue cannot be scoped by user; skip Step 1 and use the Step 3 fallback. The Step 1.5 ownership gate still applies.
|
|
40
|
+
|
|
41
|
+
### Step 1: Read the Todo Queue (pure-read)
|
|
42
|
+
|
|
43
|
+
With `USER_ID` resolved, call MCP `get_todos({ repo_id, user_id, worktree_id })` (omit `worktree_id` when `WORKTREE_ID` is `null` or unresolved — that returns only unscoped rows). Take **`rows[0]`** as the queue head (ordered by `sort_order`).
|
|
24
44
|
|
|
25
|
-
|
|
45
|
+
- The head carries `command`, `instructions`, `state`, `metadata`, `worktree_id`, `checkpoint_id`, `task_id`.
|
|
46
|
+
- The routing context (checkpoint/task) lives in **`rows[0].metadata`**.
|
|
47
|
+
- `get_todos` is **pure-read** — `apps/todo-worker` is the sole regen authority. NEVER call `get_next_action` or `regenerate_todos_for_repo`.
|
|
48
|
+
- Empty array, or `USER_ID` unavailable → go to Step 3 (empty-queue fallback).
|
|
26
49
|
|
|
27
|
-
|
|
50
|
+
Queue `command` values may use the `/codebyplan:<name>` plugin-namespace form (e.g. `/codebyplan:round-start`); treat each as the matching `/cbp-<name>` skill for the Step 2 matrix.
|
|
28
51
|
|
|
29
|
-
### Step 1.5:
|
|
52
|
+
### Step 1.5: Resolve Target + Worktree-Ownership Gate
|
|
30
53
|
|
|
31
|
-
|
|
54
|
+
Resolve the routing target's checkpoint and gate on ownership BEFORE any auto-trigger — including the Step 1.6 planning hand-offs. Refuse to route into, plan, or start work locked to a different worktree.
|
|
32
55
|
|
|
33
|
-
|
|
56
|
+
Resolve the checkpoint from `rows[0].metadata` (or MCP `get_current_task`), then load its `worktree_id` + `plan` + `status` via MCP `get_checkpoints` and its task count via MCP `get_tasks(checkpoint_id)`. This single load is reused by the Step 1.6 planning gate. Skip this gate when the routing target has no checkpoint (idle — see Step 3) or the command is `/cbp-session-start`.
|
|
34
57
|
|
|
35
|
-
|
|
36
|
-
- **RULE B — planned-but-pending**: has tasks (or non-empty `plan.steps[]`) **BUT** `status === "pending"` (not yet activated) → the checkpoint is planned but not started. Suppress the Step-1 command; surface `Now starting CHK-NNN… handing off to /cbp-checkpoint-start` and auto-trigger `/cbp-checkpoint-start {NNN}` (a planned checkpoint must be started + claimed before task work).
|
|
37
|
-
- **Neither** (planned AND `active`) → fall through to Step 2 unchanged. No regression to the existing flow.
|
|
58
|
+
Two ownership signals:
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
1. **Server-detected conflict** — `rows[0].state === "worktree_conflict"` (command `/codebyplan:release-or-switch`): the todo-worker already detected the lock conflict. The owner is in `rows[0].metadata.conflicting_worktree_name` / `.conflicting_worktree_id`. Surface the mismatch message below and **STOP**. Do NOT auto-trigger the release-or-switch command and do NOT call `release_assignment` — switch worktrees rather than reassigning a lock to the caller.
|
|
61
|
+
2. **Checkpoint-ownership check** — take the target checkpoint's `worktree_id` (loaded above, keyed by `rows[0].checkpoint_id` / `metadata.checkpoint.id`) and compare with caller `WORKTREE_ID`:
|
|
62
|
+
- target `worktree_id` is `null` → **allow** (unassigned — any worktree may pick it up; covers the both-null main-repo case).
|
|
63
|
+
- target `worktree_id` === caller `WORKTREE_ID` → **allow**.
|
|
64
|
+
- target `worktree_id` non-null **AND** caller `WORKTREE_ID` is `null`/unresolved → **block** (deliberate safety: identity cannot be confirmed. This does not contradict Step 0 — reading the queue is fine, auto-triggering INTO assigned work is not. Run `npx codebyplan setup` to register this worktree).
|
|
65
|
+
- target `worktree_id` non-null and differs from a non-null caller → **block**.
|
|
66
|
+
|
|
67
|
+
On block, resolve the owning worktree's `name` + `path` via MCP `get_worktrees({ repo_id })` (match by id), then emit and STOP:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
⚠ Work mismatch: CHK-<NNN> TASK-<N> is assigned to worktree <name> (<short-uuid>), not this one (<this-name> / <this-short-uuid>).
|
|
71
|
+
Options:
|
|
72
|
+
A) Switch to <owning-worktree-path> and resume there
|
|
73
|
+
B) Work on something else here — run /cbp-checkpoint-create or /cbp-task-create
|
|
74
|
+
C) /cbp-session-end
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`<short-uuid>` = first 8 chars of the worktree UUID. Caller unresolved → render "this one" as `(unresolved / —)`. Name lookup miss → show the UUID alone. Wait for the user. NEVER propose reassigning the checkpoint to the caller worktree.
|
|
78
|
+
|
|
79
|
+
### Step 1.6: Checkpoint Planning Gate
|
|
80
|
+
|
|
81
|
+
Ownership passed (Step 1.5). Now gate on the checkpoint's planning + activation state — reusing the `plan` + `status` + task count loaded in Step 1.5 — so work never starts on a half-baked or un-activated checkpoint. Evaluate two rules in order (Rule A wins if both could match):
|
|
82
|
+
|
|
83
|
+
- **RULE A — unplanned**: empty `plan.steps[]` **AND** zero tasks → not planned. Suppress the head command; surface `Now planning CHK-NNN… handing off to /cbp-checkpoint-plan` and auto-trigger `/cbp-checkpoint-plan {NNN}`.
|
|
84
|
+
- **RULE B — planned-but-pending**: has tasks (or non-empty `plan.steps[]`) **BUT** `status === "pending"` (not yet activated) → planned, not started. Suppress the head command; surface `Now starting CHK-NNN… handing off to /cbp-checkpoint-start` and auto-trigger `/cbp-checkpoint-start {NNN}` (a planned checkpoint must be started + claimed before task work).
|
|
85
|
+
- **Neither** (planned AND `active`) → fall through to Step 2.
|
|
86
|
+
|
|
87
|
+
Skip this gate when the routing target has no checkpoint (idle — see Step 3) or the command is `/cbp-session-start`.
|
|
40
88
|
|
|
41
89
|
### Step 2: Load Context Based on Command
|
|
42
90
|
|
|
43
|
-
|
|
91
|
+
Once the gates pass, load the context the head command needs. This ensures `/clear` + `/cbp-todo` reliably restores full working context.
|
|
44
92
|
|
|
45
|
-
**Use the context loading matrix below.** Match the `command`
|
|
93
|
+
**Use the context loading matrix below.** Match the `command` (in its `/cbp-<name>` form) to determine what to load.
|
|
46
94
|
|
|
47
95
|
| Command Pattern | Context to Load |
|
|
48
96
|
|----------------|-----------------|
|
|
49
97
|
| `/cbp-session-start` | None — `/cbp-session-start` handles its own loading |
|
|
50
|
-
| `/cbp-checkpoint-create` | If checkpoint exists in
|
|
98
|
+
| `/cbp-checkpoint-create` | If checkpoint exists in `rows[0].metadata`: load checkpoint via MCP `get_checkpoints` (filter by number). Display checkpoint title, goal, ideas summary |
|
|
51
99
|
| `/cbp-checkpoint-plan` | Load checkpoint via MCP `get_checkpoints` (filter by number) + `get_tasks(checkpoint_id)`. Display checkpoint title, goal, ideas, existing task count |
|
|
52
100
|
| `/cbp-checkpoint-start` | Load checkpoint via MCP `get_checkpoints` + `get_tasks(checkpoint_id)`. Display checkpoint title, status, claim state, first pending task |
|
|
53
101
|
| `/cbp-task-start [N]` | Load via MCP `get_current_task`. Display checkpoint title + task title/requirements summary |
|
|
@@ -87,9 +135,13 @@ Display a brief context summary:
|
|
|
87
135
|
[Brief: what was done, what passed/failed]
|
|
88
136
|
```
|
|
89
137
|
|
|
90
|
-
### Step 3:
|
|
138
|
+
### Step 3: Empty Queue — Fallback, then Idle
|
|
139
|
+
|
|
140
|
+
Reached when `get_todos` returns `[]` or `USER_ID` was unavailable.
|
|
91
141
|
|
|
92
|
-
|
|
142
|
+
1. **Fallback discovery** (worktree-scoped): MCP `get_current_task({ repo_id, worktree_id })` and `get_checkpoints({ repo_id, worktree_id, status: 'active' })` to discover whether actionable work exists for this caller.
|
|
143
|
+
2. **Actionable work found** → treat the discovered checkpoint as the routing target and apply BOTH the Step 1.5 ownership gate and the Step 1.6 planning gate to it, using the `worktree_id` + `plan` + `status` returned by `get_checkpoints` (the fallback has no `rows[0]` — substitute the discovered checkpoint). If both gates pass, route via Step 2 → Step 4.
|
|
144
|
+
3. **Nothing actionable** → suggest ending the session:
|
|
93
145
|
|
|
94
146
|
```
|
|
95
147
|
No work remaining in the current checkpoint.
|
|
@@ -98,14 +150,15 @@ Run `/cbp-session-end` to finalize the session log and close out.
|
|
|
98
150
|
Or run `/cbp-checkpoint-create` to start new work.
|
|
99
151
|
```
|
|
100
152
|
|
|
101
|
-
Wait for the user. Do not auto-trigger `/cbp-session-end` — session wrap-up is a user-driven decision.
|
|
153
|
+
Wait for the user. Do not auto-trigger `/cbp-session-end` — session wrap-up is a user-driven decision. NEVER call `regenerate_todos_for_repo`; an empty queue is a legitimate "nothing scheduled" state (the todo-worker has not regenerated yet, or there is no actionable work).
|
|
102
154
|
|
|
103
155
|
### Step 4: Display and Auto-trigger
|
|
104
156
|
|
|
105
|
-
|
|
157
|
+
Reached only when the Step 1.5 ownership gate allowed routing to continue and the Step 1.6 planning gate fell through (no hand-off). Show `rows[0].instructions` (so the user sees what is happening), then auto-trigger `rows[0].command` (its `/cbp-<name>` form).
|
|
106
158
|
|
|
107
159
|
## Integration
|
|
108
160
|
|
|
109
161
|
- **Called by**: `/cbp-session-start`, `/cbp-task-complete`, `/cbp-checkpoint-complete`, manual, after `/clear`
|
|
110
|
-
- **
|
|
111
|
-
- **
|
|
162
|
+
- **Resolves**: `npx codebyplan resolve-worktree --json` (worktree id + distress signal), `npx codebyplan whoami --json` (user id)
|
|
163
|
+
- **Reads**: MCP `get_todos`, `get_current_task`, `get_rounds`, `get_checkpoints`, `get_tasks`, `get_worktrees`
|
|
164
|
+
- **Triggers**: `rows[0].command` (auto, after the Step 1.5 ownership gate); Step 1.6 overrides to `/cbp-checkpoint-plan` (unplanned) or `/cbp-checkpoint-start` (planned-but-pending)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
scope: org-shared
|
|
3
|
+
name: cbp-todo-qa-regression
|
|
4
|
+
description: Manual regression procedure for the cbp-todo worktree-ownership gate + get_todos switch (CHK-137 TASK-2)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# cbp-todo — Worktree-Ownership Regression
|
|
8
|
+
|
|
9
|
+
Manual procedure verifying that `/cbp-todo` reads the pure-read `get_todos` queue and refuses to auto-route into work locked to another worktree. No automated harness exists for markdown skills; run these by hand (or read the queue with the MCP tools) whenever Step 0/1/1.5 of `SKILL.md` changes.
|
|
10
|
+
|
|
11
|
+
Repo under test: `2ff6d405-39c5-47b8-a6d1-59f998ac0537`. Resolve a real `user_id` with `npx codebyplan whoami --json`, or harvest a non-null `assigned_user_id` from `get_checkpoints`.
|
|
12
|
+
|
|
13
|
+
## Preconditions
|
|
14
|
+
|
|
15
|
+
- `get_todos` is the only Step 1 read — confirm no `get_next_action` / `regenerate_todos_for_repo` call remains (`grep -n 'get_next_action\|regenerate_todos_for_repo' SKILL.md` → no hits).
|
|
16
|
+
- Step 0 uses `resolve-worktree --json` and `whoami --json`.
|
|
17
|
+
|
|
18
|
+
## Scenario A — caller owns the work → auto-trigger
|
|
19
|
+
|
|
20
|
+
1. From a worktree that owns the active checkpoint (caller `WORKTREE_ID` === target checkpoint `worktree_id`, or target `worktree_id` is `null`).
|
|
21
|
+
2. `get_todos({ repo_id, user_id, worktree_id })` returns a head whose target checkpoint is owned by the caller (or unscoped).
|
|
22
|
+
3. **Expected**: Step 1.5 ownership gate allows, Step 1.6 planning gate falls through, Step 4 auto-triggers `rows[0].command` (mapped to its `/cbp-<name>` form).
|
|
23
|
+
|
|
24
|
+
## Scenario B1 — server-generated conflict head → halt
|
|
25
|
+
|
|
26
|
+
1. Caller `codebyplan-claude-2` (`38cd7dfa`). Active work assigned to `codebyplan-cli` (`016bd7f2`).
|
|
27
|
+
2. `get_todos` head is the server conflict todo: `state === "worktree_conflict"`, `command "/codebyplan:release-or-switch"`, `metadata.conflicting_worktree_name === "codebyplan-cli"`.
|
|
28
|
+
3. **MCP calls expected**: `get_todos`; `get_worktrees` (to resolve `016bd7f2` path for the message). NO `get_checkpoints` needed — the server already resolved the conflict.
|
|
29
|
+
4. **Expected**: Step 1.5 server-conflict branch blocks and STOPS. The "Work mismatch" message names `codebyplan-cli (016bd7f2)` vs `codebyplan-claude-2 (38cd7dfa)`, offers Switch (`/Users/merilyviks/codebyplan-cli`) / other-work / session-end. NO auto-trigger; NO `release_assignment` call (never reassign the lock to the caller).
|
|
30
|
+
|
|
31
|
+
## Scenario B2 — normal head, mismatched checkpoint owner → halt
|
|
32
|
+
|
|
33
|
+
1. Caller `codebyplan-claude-2` (`38cd7dfa`). `get_todos` head is a normal routing todo whose target checkpoint `worktree_id` is `016bd7f2`.
|
|
34
|
+
2. **MCP calls expected**: `get_todos`; `get_checkpoints` (Step 1.5 load, reads the target `worktree_id`); `get_worktrees` (resolve `016bd7f2` name + path).
|
|
35
|
+
3. **Expected**: Step 1.5 checkpoint-ownership branch blocks and STOPS with the same "Work mismatch" message. NO auto-trigger; the Step 1.6 planning gate never runs (ownership precedes it).
|
|
36
|
+
|
|
37
|
+
## Scenario C — empty queue + cross-worktree current task → halt
|
|
38
|
+
|
|
39
|
+
1. `get_todos` returns `[]` (or `whoami` returned `null`, so Step 1 was skipped).
|
|
40
|
+
2. Step 3 fallback: `get_current_task({ repo_id, worktree_id })` / `get_checkpoints({ repo_id, worktree_id, status: 'active' })` surface a checkpoint whose `worktree_id` differs from the caller.
|
|
41
|
+
3. **Expected**: the Step 1.5 ownership gate (applied to the fallback target) blocks the discovered work with the same "Work mismatch" message. NO auto-trigger. `regenerate_todos_for_repo` is never called.
|
|
42
|
+
|
|
43
|
+
## Edge — both null
|
|
44
|
+
|
|
45
|
+
Caller `WORKTREE_ID` empty AND target checkpoint `worktree_id` `null` → legitimate main-repo / unassigned work → auto-trigger allowed.
|