codebyplan 1.13.9 → 1.13.10

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.13.9";
17
+ VERSION = "1.13.10";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -148,6 +148,7 @@ var init_gitignore_block = __esm({
148
148
  ".claude/scheduled_tasks.lock",
149
149
  ".codebyplan/device.local.json",
150
150
  ".codebyplan/statusline.local.json",
151
+ ".codebyplan/worktree.local.json",
151
152
  ".codebyplan.local.json"
152
153
  ];
153
154
  GITIGNORE_BLOCK_START = "# >>> codebyplan (managed) >>>";
@@ -3409,6 +3410,81 @@ var init_sync_approvals = __esm({
3409
3410
  }
3410
3411
  });
3411
3412
 
3413
+ // src/lib/worktree-cache.ts
3414
+ import { mkdir as mkdir5, readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
3415
+ import { dirname as dirname4, join as join11 } from "node:path";
3416
+ function worktreeCachePath(repoRoot) {
3417
+ return join11(repoRoot, ".codebyplan", "worktree.local.json");
3418
+ }
3419
+ async function readCachedWorktreeId(repoRoot, currentBranch) {
3420
+ const cachePath = worktreeCachePath(repoRoot);
3421
+ let raw;
3422
+ try {
3423
+ raw = await readFile11(cachePath, "utf-8");
3424
+ } catch (err) {
3425
+ const code = err.code;
3426
+ if (code === "ENOENT") {
3427
+ return null;
3428
+ }
3429
+ console.warn(
3430
+ `worktree-cache: read failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`
3431
+ );
3432
+ return null;
3433
+ }
3434
+ let parsed;
3435
+ try {
3436
+ parsed = JSON.parse(raw);
3437
+ } catch {
3438
+ console.warn(
3439
+ "worktree-cache: JSON parse failed (non-fatal): cache entry will be ignored"
3440
+ );
3441
+ return null;
3442
+ }
3443
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed) || typeof parsed.worktree_id !== "string" || typeof parsed.branch !== "string" || typeof parsed.device_id !== "string" || typeof parsed.resolved_at !== "string") {
3444
+ return null;
3445
+ }
3446
+ const data = parsed;
3447
+ if (!data.branch || data.branch !== currentBranch) {
3448
+ return null;
3449
+ }
3450
+ return data.worktree_id;
3451
+ }
3452
+ async function writeWorktreeCache(repoRoot, data) {
3453
+ if (!data.branch) {
3454
+ console.warn(
3455
+ "worktree-cache: skipping write for empty branch (detached HEAD or non-repo)"
3456
+ );
3457
+ return false;
3458
+ }
3459
+ const cachePath = worktreeCachePath(repoRoot);
3460
+ const dirPath = dirname4(cachePath);
3461
+ const payload = {
3462
+ worktree_id: data.worktree_id,
3463
+ branch: data.branch,
3464
+ device_id: data.device_id,
3465
+ resolved_at: (/* @__PURE__ */ new Date()).toISOString()
3466
+ };
3467
+ try {
3468
+ await mkdir5(dirPath, { recursive: true });
3469
+ await writeFile9(
3470
+ cachePath,
3471
+ JSON.stringify(payload, null, 2) + "\n",
3472
+ "utf-8"
3473
+ );
3474
+ return true;
3475
+ } catch (err) {
3476
+ console.warn(
3477
+ `worktree-cache: write failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`
3478
+ );
3479
+ return false;
3480
+ }
3481
+ }
3482
+ var init_worktree_cache = __esm({
3483
+ "src/lib/worktree-cache.ts"() {
3484
+ "use strict";
3485
+ }
3486
+ });
3487
+
3412
3488
  // src/cli/round.ts
3413
3489
  var round_exports = {};
3414
3490
  __export(round_exports, {
@@ -3420,7 +3496,7 @@ __export(round_exports, {
3420
3496
  setRetryDelayMs: () => setRetryDelayMs
3421
3497
  });
3422
3498
  import { access as access3 } from "node:fs/promises";
3423
- import { join as join11 } from "node:path";
3499
+ import { join as join12 } from "node:path";
3424
3500
  import { execSync as execSync2 } from "node:child_process";
3425
3501
  function setRetryDelayMs(ms) {
3426
3502
  RETRY_DELAY_MS = ms;
@@ -3480,6 +3556,34 @@ function printRoundHelp() {
3480
3556
  "\n codebyplan round <subcommand>\n\n Subcommands:\n sync-approvals Sync git diff and approvals with round/task state\n\n sync-approvals flags:\n --round-id <uuid> Round UUID (required)\n --task-id <uuid> Task UUID (required)\n --dry-run Print merged payload to stdout without writing\n\n"
3481
3557
  );
3482
3558
  }
3559
+ async function resolveCallerWorktreeId(repoRoot, currentBranch, repoId, overrideId) {
3560
+ if (overrideId) {
3561
+ return overrideId;
3562
+ }
3563
+ const cached = await readCachedWorktreeId(repoRoot, currentBranch);
3564
+ if (cached) {
3565
+ return cached;
3566
+ }
3567
+ if (!repoId || !currentBranch) {
3568
+ return null;
3569
+ }
3570
+ const deviceId = await getOrCreateDeviceId(repoRoot);
3571
+ const wid = await resolveWorktreeId({
3572
+ repoId,
3573
+ repoPath: repoRoot,
3574
+ branch: currentBranch,
3575
+ deviceId
3576
+ });
3577
+ if (wid) {
3578
+ await writeWorktreeCache(repoRoot, {
3579
+ worktree_id: wid,
3580
+ branch: currentBranch,
3581
+ device_id: deviceId
3582
+ });
3583
+ return wid;
3584
+ }
3585
+ return null;
3586
+ }
3483
3587
  function parseFlagsFromArgs(args) {
3484
3588
  const flags = {};
3485
3589
  const booleans = /* @__PURE__ */ new Set();
@@ -3503,6 +3607,7 @@ async function runRoundSyncApprovals(args) {
3503
3607
  const roundId = flags["round-id"];
3504
3608
  const taskId = flags["task-id"];
3505
3609
  const dryRun = booleans.has("dry-run");
3610
+ const overrideWorktreeId = flags["caller-worktree-id"];
3506
3611
  if (!roundId) {
3507
3612
  process.stderr.write(
3508
3613
  "sync-approvals: --round-id is required\nUsage: codebyplan round sync-approvals --round-id <uuid> --task-id <uuid>\n"
@@ -3523,6 +3628,14 @@ async function runRoundSyncApprovals(args) {
3523
3628
  // Walk up to the directory containing .codebyplan/ or .codebyplan.json
3524
3629
  found.path.replace(/\/.codebyplan(\.json|\/repo\.json)$/, "")
3525
3630
  ) : process.cwd();
3631
+ let currentBranch = "";
3632
+ try {
3633
+ currentBranch = execSync2("git symbolic-ref --short HEAD", {
3634
+ cwd: repoRoot,
3635
+ encoding: "utf-8"
3636
+ }).trim();
3637
+ } catch {
3638
+ }
3526
3639
  let rounds;
3527
3640
  try {
3528
3641
  rounds = await fetchRoundsWithRetry(taskId);
@@ -3542,6 +3655,17 @@ async function runRoundSyncApprovals(args) {
3542
3655
  }
3543
3656
  const taskResponse = await apiGet(`/tasks/${taskId}`);
3544
3657
  const currentTask = taskResponse.data;
3658
+ const callerWorktreeId = await resolveCallerWorktreeId(
3659
+ repoRoot,
3660
+ currentBranch,
3661
+ found?.contents.repo_id,
3662
+ overrideWorktreeId
3663
+ );
3664
+ if (!dryRun && !callerWorktreeId) {
3665
+ throw new Error(
3666
+ "could not resolve caller_worktree_id for this worktree.\n Run: codebyplan resolve-worktree --cache\n If this worktree is not registered, run: npx codebyplan setup\n Then re-run /cbp-round-update (sync-approvals)."
3667
+ );
3668
+ }
3545
3669
  let gitStatusOutput = "";
3546
3670
  try {
3547
3671
  gitStatusOutput = execSync2("git status --short --porcelain -z", {
@@ -3553,7 +3677,7 @@ async function runRoundSyncApprovals(args) {
3553
3677
  "sync-approvals: git status failed; proceeding with empty diff\n"
3554
3678
  );
3555
3679
  }
3556
- const hookPath = join11(
3680
+ const hookPath = join12(
3557
3681
  repoRoot,
3558
3682
  ".claude",
3559
3683
  "hooks",
@@ -3586,12 +3710,14 @@ async function runRoundSyncApprovals(args) {
3586
3710
  } else {
3587
3711
  await mcpCall("update_round", {
3588
3712
  round_id: roundId,
3589
- files_changed: result.merged_files_changed
3713
+ files_changed: result.merged_files_changed,
3714
+ caller_worktree_id: callerWorktreeId
3590
3715
  });
3591
3716
  await mcpCall("update_task", {
3592
3717
  task_id: taskId,
3593
3718
  files_changed: result.merged_files_changed,
3594
- app_file_approval_by_user: false
3719
+ app_file_approval_by_user: false,
3720
+ caller_worktree_id: callerWorktreeId
3595
3721
  });
3596
3722
  stdoutPayload = JSON.stringify(
3597
3723
  {
@@ -3634,13 +3760,16 @@ var init_round = __esm({
3634
3760
  init_mcp_client();
3635
3761
  init_flags();
3636
3762
  init_sync_approvals();
3763
+ init_worktree_cache();
3764
+ init_resolve_worktree();
3765
+ init_local_config();
3637
3766
  RETRY_DELAY_MS = 1e3;
3638
3767
  }
3639
3768
  });
3640
3769
 
3641
3770
  // src/lib/migrate-branch-model.ts
3642
- import { readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
3643
- import { join as join12 } from "node:path";
3771
+ import { readFile as readFile12, writeFile as writeFile10 } from "node:fs/promises";
3772
+ import { join as join13 } from "node:path";
3644
3773
  import { execSync as execSync3 } from "node:child_process";
3645
3774
  function assertValidBranchName(branch) {
3646
3775
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3650,7 +3779,7 @@ function assertValidBranchName(branch) {
3650
3779
  }
3651
3780
  }
3652
3781
  async function readJsonFile(filePath) {
3653
- const raw = await readFile11(filePath, "utf-8");
3782
+ const raw = await readFile12(filePath, "utf-8");
3654
3783
  const parsed = JSON.parse(raw);
3655
3784
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3656
3785
  throw new Error(`${filePath} does not contain a JSON object`);
@@ -3719,12 +3848,12 @@ async function runBranchMigration(opts) {
3719
3848
  if (found) {
3720
3849
  if (found.path.endsWith("/repo.json")) {
3721
3850
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3722
- configPath = join12(dir, "git.json");
3851
+ configPath = join13(dir, "git.json");
3723
3852
  } else {
3724
3853
  configPath = found.path;
3725
3854
  }
3726
3855
  } else {
3727
- configPath = join12(cwd, ".codebyplan", "git.json");
3856
+ configPath = join13(cwd, ".codebyplan", "git.json");
3728
3857
  }
3729
3858
  let fileRaw;
3730
3859
  let fileParsed;
@@ -3798,7 +3927,7 @@ async function runBranchMigration(opts) {
3798
3927
  const updatedParsed = { ...fileParsed, branch_config: after };
3799
3928
  const newJson = JSON.stringify(updatedParsed, null, 2) + "\n";
3800
3929
  if (newJson !== fileRaw) {
3801
- await writeFile9(configPath, newJson, "utf-8");
3930
+ await writeFile10(configPath, newJson, "utf-8");
3802
3931
  }
3803
3932
  }
3804
3933
  return {
@@ -3904,8 +4033,8 @@ var init_branch = __esm({
3904
4033
  });
3905
4034
 
3906
4035
  // src/lib/git-utils.ts
3907
- import { readFile as readFile12 } from "node:fs/promises";
3908
- import { join as join13 } from "node:path";
4036
+ import { readFile as readFile13 } from "node:fs/promises";
4037
+ import { join as join14 } from "node:path";
3909
4038
  import { spawnSync as spawnSync2 } from "node:child_process";
3910
4039
  async function readBaseBranch(cwd) {
3911
4040
  const found = await findCodebyplanConfig(cwd);
@@ -3913,15 +4042,15 @@ async function readBaseBranch(cwd) {
3913
4042
  if (found) {
3914
4043
  if (found.path.endsWith("/repo.json")) {
3915
4044
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3916
- gitJsonPath = join13(dir, "git.json");
4045
+ gitJsonPath = join14(dir, "git.json");
3917
4046
  } else {
3918
4047
  gitJsonPath = found.path;
3919
4048
  }
3920
4049
  } else {
3921
- gitJsonPath = join13(cwd, ".codebyplan", "git.json");
4050
+ gitJsonPath = join14(cwd, ".codebyplan", "git.json");
3922
4051
  }
3923
4052
  try {
3924
- const raw = await readFile12(gitJsonPath, "utf-8");
4053
+ const raw = await readFile13(gitJsonPath, "utf-8");
3925
4054
  const parsed = JSON.parse(raw);
3926
4055
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3927
4056
  return "main";
@@ -3959,8 +4088,8 @@ var init_git_utils = __esm({
3959
4088
  });
3960
4089
 
3961
4090
  // src/lib/bump.ts
3962
- import { readFile as readFile13, writeFile as writeFile10, access as access4, readdir as readdir3 } from "node:fs/promises";
3963
- import { join as join14, relative as relative3, resolve as resolve2 } from "node:path";
4091
+ import { readFile as readFile14, writeFile as writeFile11, access as access4, readdir as readdir3 } from "node:fs/promises";
4092
+ import { join as join15, relative as relative3, resolve as resolve2 } from "node:path";
3964
4093
  import { spawnSync as spawnSync3 } from "node:child_process";
3965
4094
  function parsePnpmWorkspaceGlobs(raw) {
3966
4095
  const lines = raw.split("\n");
@@ -3990,18 +4119,18 @@ async function expandGlob(cwd, glob) {
3990
4119
  if (parts.length !== 2 || parts[1] !== "*") {
3991
4120
  return [];
3992
4121
  }
3993
- const parentDir = join14(cwd, parts[0]);
4122
+ const parentDir = join15(cwd, parts[0]);
3994
4123
  let dirs;
3995
4124
  try {
3996
4125
  const entries = await readdir3(parentDir, { withFileTypes: true });
3997
- dirs = entries.filter((e) => e.isDirectory()).map((e) => join14(parentDir, e.name));
4126
+ dirs = entries.filter((e) => e.isDirectory()).map((e) => join15(parentDir, e.name));
3998
4127
  } catch {
3999
4128
  return [];
4000
4129
  }
4001
4130
  const results = [];
4002
4131
  for (const dir of dirs) {
4003
4132
  try {
4004
- await access4(join14(dir, "package.json"));
4133
+ await access4(join15(dir, "package.json"));
4005
4134
  results.push(dir);
4006
4135
  } catch {
4007
4136
  }
@@ -4012,7 +4141,7 @@ async function buildPackageMap(cwd) {
4012
4141
  const map = /* @__PURE__ */ new Map();
4013
4142
  let globs = [];
4014
4143
  try {
4015
- const raw = await readFile13(join14(cwd, "pnpm-workspace.yaml"), "utf-8");
4144
+ const raw = await readFile14(join15(cwd, "pnpm-workspace.yaml"), "utf-8");
4016
4145
  globs = parsePnpmWorkspaceGlobs(raw);
4017
4146
  } catch {
4018
4147
  }
@@ -4020,7 +4149,7 @@ async function buildPackageMap(cwd) {
4020
4149
  const dirs = await expandGlob(cwd, glob);
4021
4150
  for (const dir of dirs) {
4022
4151
  try {
4023
- const pkgRaw = await readFile13(join14(dir, "package.json"), "utf-8");
4152
+ const pkgRaw = await readFile14(join15(dir, "package.json"), "utf-8");
4024
4153
  const pkg = JSON.parse(pkgRaw);
4025
4154
  const name = pkg.name ?? relative3(cwd, dir);
4026
4155
  map.set(dir, { name, dir });
@@ -4110,7 +4239,7 @@ function injectVersion(raw, nextVersion, filePath) {
4110
4239
  async function prependChangelog(changelogPath, packageName, nextVersion, now, dryRun) {
4111
4240
  let existing;
4112
4241
  try {
4113
- existing = await readFile13(changelogPath, "utf-8");
4242
+ existing = await readFile14(changelogPath, "utf-8");
4114
4243
  } catch {
4115
4244
  return false;
4116
4245
  }
@@ -4123,7 +4252,7 @@ async function prependChangelog(changelogPath, packageName, nextVersion, now, dr
4123
4252
  `;
4124
4253
  const updated = entry + existing;
4125
4254
  if (!dryRun) {
4126
- await writeFile10(changelogPath, updated, "utf-8");
4255
+ await writeFile11(changelogPath, updated, "utf-8");
4127
4256
  }
4128
4257
  return true;
4129
4258
  }
@@ -4149,7 +4278,7 @@ async function runBump(opts) {
4149
4278
  const changedFiles = (diffResult.stdout ?? "").trim().split("\n").filter(Boolean).map((f) => resolve2(cwd, f));
4150
4279
  const packageMap = await buildPackageMap(cwd);
4151
4280
  const packageDirs = Array.from(packageMap.keys());
4152
- const rootPkgPath = join14(cwd, "package.json");
4281
+ const rootPkgPath = join15(cwd, "package.json");
4153
4282
  const changedPackageDirs = /* @__PURE__ */ new Set();
4154
4283
  for (const absFile of changedFiles) {
4155
4284
  const owner = findOwningPackage(absFile, packageDirs);
@@ -4160,19 +4289,19 @@ async function runBump(opts) {
4160
4289
  const entries = [];
4161
4290
  for (const pkgDir of changedPackageDirs) {
4162
4291
  const pkgInfo = packageMap.get(pkgDir);
4163
- const pkgJsonPath = join14(pkgDir, "package.json");
4292
+ const pkgJsonPath = join15(pkgDir, "package.json");
4164
4293
  if (pkgJsonPath === rootPkgPath) continue;
4165
4294
  const versionFileCandidates = [
4166
4295
  { abs: pkgJsonPath, rel: relative3(cwd, pkgJsonPath).replace(/\\/g, "/") }
4167
4296
  ];
4168
- const tauriConfPath = join14(pkgDir, "src-tauri", "tauri.conf.json");
4297
+ const tauriConfPath = join15(pkgDir, "src-tauri", "tauri.conf.json");
4169
4298
  const tauriRelPath = relative3(cwd, tauriConfPath).replace(/\\/g, "/");
4170
4299
  try {
4171
4300
  await access4(tauriConfPath);
4172
4301
  versionFileCandidates.push({ abs: tauriConfPath, rel: tauriRelPath });
4173
4302
  } catch {
4174
4303
  }
4175
- const appJsonPath = join14(pkgDir, "app.json");
4304
+ const appJsonPath = join15(pkgDir, "app.json");
4176
4305
  const appJsonRelPath = relative3(cwd, appJsonPath).replace(/\\/g, "/");
4177
4306
  try {
4178
4307
  await access4(appJsonPath);
@@ -4181,7 +4310,7 @@ async function runBump(opts) {
4181
4310
  }
4182
4311
  let currentPkgJsonRaw;
4183
4312
  try {
4184
- currentPkgJsonRaw = await readFile13(pkgJsonPath, "utf-8");
4313
+ currentPkgJsonRaw = await readFile14(pkgJsonPath, "utf-8");
4185
4314
  } catch (err) {
4186
4315
  console.warn(
4187
4316
  `runBump: could not read ${pkgJsonPath}: ${err instanceof Error ? err.message : String(err)}`
@@ -4226,7 +4355,7 @@ async function runBump(opts) {
4226
4355
  for (const { abs, rel } of versionFileCandidates) {
4227
4356
  let raw;
4228
4357
  try {
4229
- raw = await readFile13(abs, "utf-8");
4358
+ raw = await readFile14(abs, "utf-8");
4230
4359
  } catch {
4231
4360
  continue;
4232
4361
  }
@@ -4240,11 +4369,11 @@ async function runBump(opts) {
4240
4369
  }
4241
4370
  const updated = injectVersion(raw, nextVersion, abs);
4242
4371
  if (!dryRun) {
4243
- await writeFile10(abs, updated, "utf-8");
4372
+ await writeFile11(abs, updated, "utf-8");
4244
4373
  }
4245
4374
  updatedVersionFiles.push(rel);
4246
4375
  }
4247
- const changelogPath = join14(pkgDir, "CHANGELOG.md");
4376
+ const changelogPath = join15(pkgDir, "CHANGELOG.md");
4248
4377
  const changelogUpdated = await prependChangelog(
4249
4378
  changelogPath,
4250
4379
  pkgInfo.name,
@@ -5612,6 +5741,7 @@ function distress(kind, message, jsonMode) {
5612
5741
  }
5613
5742
  async function runResolveWorktree() {
5614
5743
  const jsonMode = hasFlag("json", 3);
5744
+ const cacheMode = hasFlag("cache", 3);
5615
5745
  let errorContext = null;
5616
5746
  const migrationNoticeCallback = (legacyPath, primaryPath) => {
5617
5747
  if (!jsonMode) {
@@ -5625,7 +5755,7 @@ async function runResolveWorktree() {
5625
5755
  const projectPath = process.cwd();
5626
5756
  const found = await findCodebyplanConfig(projectPath);
5627
5757
  if (!found?.contents.repo_id) {
5628
- emitAndExit(null, null, jsonMode);
5758
+ emitAndExit(null, null, jsonMode, cacheMode ? false : void 0);
5629
5759
  }
5630
5760
  const repoId = found.contents.repo_id;
5631
5761
  try {
@@ -5656,7 +5786,7 @@ async function runResolveWorktree() {
5656
5786
  message: deviceErr instanceof Error ? deviceErr.message : String(deviceErr)
5657
5787
  };
5658
5788
  }
5659
- emitAndExit(null, errorContext, jsonMode);
5789
+ emitAndExit(null, errorContext, jsonMode, cacheMode ? false : void 0);
5660
5790
  }
5661
5791
  let branch = "";
5662
5792
  try {
@@ -5685,7 +5815,12 @@ async function runResolveWorktree() {
5685
5815
  onError: onResolverError
5686
5816
  });
5687
5817
  if (worktreeId) {
5688
- emitAndExit(worktreeId, errorContext, jsonMode);
5818
+ const cacheWritten = cacheMode ? await writeWorktreeCache(projectPath, {
5819
+ worktree_id: worktreeId,
5820
+ branch,
5821
+ device_id: deviceId
5822
+ }) : void 0;
5823
+ emitAndExit(worktreeId, errorContext, jsonMode, cacheWritten);
5689
5824
  }
5690
5825
  const useFallback = hasFlag("fallback-from-branch", 3);
5691
5826
  if (useFallback) {
@@ -5696,23 +5831,33 @@ async function runResolveWorktree() {
5696
5831
  onError: onResolverError
5697
5832
  });
5698
5833
  if (fallbackId) {
5699
- emitAndExit(fallbackId, errorContext, jsonMode);
5834
+ const cacheWritten = cacheMode ? await writeWorktreeCache(projectPath, {
5835
+ worktree_id: fallbackId,
5836
+ branch,
5837
+ device_id: deviceId
5838
+ }) : void 0;
5839
+ emitAndExit(fallbackId, errorContext, jsonMode, cacheWritten);
5700
5840
  }
5701
5841
  }
5702
- emitAndExit(null, errorContext, jsonMode);
5842
+ emitAndExit(null, errorContext, jsonMode, cacheMode ? false : void 0);
5703
5843
  } catch (err) {
5704
5844
  if (err instanceof ProcessExitSignal) throw err;
5705
5845
  const msg = err instanceof Error ? err.message : String(err);
5706
5846
  errorContext = { kind: "unhandled", message: msg };
5707
- emitAndExit(null, errorContext, jsonMode);
5847
+ emitAndExit(null, errorContext, jsonMode, cacheMode ? false : void 0);
5708
5848
  }
5709
5849
  }
5710
- function emitAndExit(worktreeId, errorContext, jsonMode) {
5850
+ function emitAndExit(worktreeId, errorContext, jsonMode, cacheWritten) {
5711
5851
  if (jsonMode) {
5712
5852
  const errorKind = errorContext?.kind ?? (worktreeId === null ? "tuple_miss" : null);
5713
- process.stdout.write(
5714
- JSON.stringify({ worktree_id: worktreeId, error_kind: errorKind }) + "\n"
5715
- );
5853
+ const payload = {
5854
+ worktree_id: worktreeId,
5855
+ error_kind: errorKind
5856
+ };
5857
+ if (cacheWritten !== void 0) {
5858
+ payload.cache_written = cacheWritten;
5859
+ }
5860
+ process.stdout.write(JSON.stringify(payload) + "\n");
5716
5861
  } else {
5717
5862
  if (worktreeId !== null) {
5718
5863
  process.stdout.write(worktreeId);
@@ -5731,6 +5876,7 @@ var init_resolve_worktree2 = __esm({
5731
5876
  init_flags();
5732
5877
  init_local_config();
5733
5878
  init_resolve_worktree();
5879
+ init_worktree_cache();
5734
5880
  init_process_exit_signal();
5735
5881
  }
5736
5882
  });
@@ -5742,7 +5888,7 @@ __export(version_status_exports, {
5742
5888
  });
5743
5889
  import { execFileSync, execSync as execSync6 } from "node:child_process";
5744
5890
  import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
5745
- import { dirname as dirname7, join as join19 } from "node:path";
5891
+ import { dirname as dirname8, join as join20 } from "node:path";
5746
5892
  function fetchLatestVersion() {
5747
5893
  try {
5748
5894
  return execFileSync("npm", ["view", "codebyplan", "version"], {
@@ -5781,11 +5927,11 @@ function detectPackageManager2(gitRoot) {
5781
5927
  let dir = process.cwd();
5782
5928
  const stopAt = gitRoot ?? null;
5783
5929
  while (true) {
5784
- if (existsSync4(join19(dir, "pnpm-lock.yaml"))) return "pnpm";
5785
- if (existsSync4(join19(dir, "yarn.lock"))) return "yarn";
5786
- if (existsSync4(join19(dir, "package-lock.json"))) return "npm";
5930
+ if (existsSync4(join20(dir, "pnpm-lock.yaml"))) return "pnpm";
5931
+ if (existsSync4(join20(dir, "yarn.lock"))) return "yarn";
5932
+ if (existsSync4(join20(dir, "package-lock.json"))) return "npm";
5787
5933
  if (stopAt !== null && dir === stopAt) break;
5788
- const parent = dirname7(dir);
5934
+ const parent = dirname8(dir);
5789
5935
  if (parent === dir) break;
5790
5936
  dir = parent;
5791
5937
  }
@@ -5803,7 +5949,7 @@ function buildInstallCommand2(pm) {
5803
5949
  }
5804
5950
  async function resolveGuard(gitRoot, currentBranch) {
5805
5951
  if (gitRoot !== null) {
5806
- const canonicalPkgPath = join19(
5952
+ const canonicalPkgPath = join20(
5807
5953
  gitRoot,
5808
5954
  "packages",
5809
5955
  "codebyplan-package",
@@ -5827,10 +5973,10 @@ async function resolveGuard(gitRoot, currentBranch) {
5827
5973
  let gitJsonPath;
5828
5974
  if (found.path.endsWith("/repo.json")) {
5829
5975
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
5830
- gitJsonPath = join19(dir, "git.json");
5976
+ gitJsonPath = join20(dir, "git.json");
5831
5977
  } else {
5832
5978
  const legacyDir = found.path.slice(0, found.path.lastIndexOf("/"));
5833
- gitJsonPath = join19(legacyDir, ".codebyplan", "git.json");
5979
+ gitJsonPath = join20(legacyDir, ".codebyplan", "git.json");
5834
5980
  }
5835
5981
  const raw = readFileSync5(gitJsonPath, "utf-8");
5836
5982
  const parsed = JSON.parse(raw);
@@ -5970,19 +6116,19 @@ var init_cmux_sync = __esm({
5970
6116
  });
5971
6117
 
5972
6118
  // src/lib/migrate-local-config.ts
5973
- import { mkdir as mkdir5, readFile as readFile14, unlink as unlink2, writeFile as writeFile11 } from "node:fs/promises";
5974
- import { join as join20 } from "node:path";
6119
+ import { mkdir as mkdir6, readFile as readFile15, unlink as unlink2, writeFile as writeFile12 } from "node:fs/promises";
6120
+ import { join as join21 } from "node:path";
5975
6121
  function legacySharedPath(projectPath) {
5976
- return join20(projectPath, ".codebyplan.json");
6122
+ return join21(projectPath, ".codebyplan.json");
5977
6123
  }
5978
6124
  function legacyLocalPath(projectPath) {
5979
- return join20(projectPath, ".codebyplan.local.json");
6125
+ return join21(projectPath, ".codebyplan.local.json");
5980
6126
  }
5981
6127
  function newDirPath(projectPath) {
5982
- return join20(projectPath, ".codebyplan");
6128
+ return join21(projectPath, ".codebyplan");
5983
6129
  }
5984
6130
  function sentinelPath(projectPath) {
5985
- return join20(projectPath, ".codebyplan", "repo.json");
6131
+ return join21(projectPath, ".codebyplan", "repo.json");
5986
6132
  }
5987
6133
  async function statSafe(p) {
5988
6134
  const { stat: stat2 } = await import("node:fs/promises");
@@ -6021,7 +6167,7 @@ async function runLocalMigration(projectPath) {
6021
6167
  }
6022
6168
  let legacyRaw;
6023
6169
  try {
6024
- legacyRaw = await readFile14(legacySharedPath(projectPath), "utf-8");
6170
+ legacyRaw = await readFile15(legacySharedPath(projectPath), "utf-8");
6025
6171
  } catch {
6026
6172
  return {
6027
6173
  migrated: true,
@@ -6048,7 +6194,7 @@ async function runLocalMigration(projectPath) {
6048
6194
  let deviceId;
6049
6195
  let deviceWrittenByHelper = false;
6050
6196
  try {
6051
- const localRaw = await readFile14(legacyLocalPath(projectPath), "utf-8");
6197
+ const localRaw = await readFile15(legacyLocalPath(projectPath), "utf-8");
6052
6198
  const localParsed = JSON.parse(localRaw);
6053
6199
  if (typeof localParsed.device_id === "string") {
6054
6200
  deviceId = localParsed.device_id;
@@ -6056,7 +6202,7 @@ async function runLocalMigration(projectPath) {
6056
6202
  } catch {
6057
6203
  }
6058
6204
  try {
6059
- await mkdir5(newDirPath(projectPath), { recursive: true });
6205
+ await mkdir6(newDirPath(projectPath), { recursive: true });
6060
6206
  } catch (err) {
6061
6207
  const code = err.code;
6062
6208
  if (code === "ENOTDIR" || code === "EEXIST") {
@@ -6075,8 +6221,8 @@ async function runLocalMigration(projectPath) {
6075
6221
  if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
6076
6222
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
6077
6223
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
6078
- await writeFile11(
6079
- join20(projectPath, ".codebyplan", "repo.json"),
6224
+ await writeFile12(
6225
+ join21(projectPath, ".codebyplan", "repo.json"),
6080
6226
  JSON.stringify(repoJson, null, 2) + "\n",
6081
6227
  "utf-8"
6082
6228
  );
@@ -6088,8 +6234,8 @@ async function runLocalMigration(projectPath) {
6088
6234
  serverJson.auto_push_enabled = cfg.auto_push_enabled;
6089
6235
  if ("port_allocations" in cfg)
6090
6236
  serverJson.port_allocations = cfg.port_allocations;
6091
- await writeFile11(
6092
- join20(projectPath, ".codebyplan", "server.json"),
6237
+ await writeFile12(
6238
+ join21(projectPath, ".codebyplan", "server.json"),
6093
6239
  JSON.stringify(serverJson, null, 2) + "\n",
6094
6240
  "utf-8"
6095
6241
  );
@@ -6097,44 +6243,44 @@ async function runLocalMigration(projectPath) {
6097
6243
  const gitJson = {};
6098
6244
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
6099
6245
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
6100
- await writeFile11(
6101
- join20(projectPath, ".codebyplan", "git.json"),
6246
+ await writeFile12(
6247
+ join21(projectPath, ".codebyplan", "git.json"),
6102
6248
  JSON.stringify(gitJson, null, 2) + "\n",
6103
6249
  "utf-8"
6104
6250
  );
6105
6251
  filesChanged.push(".codebyplan/git.json");
6106
6252
  const shipmentJson = {};
6107
6253
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
6108
- await writeFile11(
6109
- join20(projectPath, ".codebyplan", "shipment.json"),
6254
+ await writeFile12(
6255
+ join21(projectPath, ".codebyplan", "shipment.json"),
6110
6256
  JSON.stringify(shipmentJson, null, 2) + "\n",
6111
6257
  "utf-8"
6112
6258
  );
6113
6259
  filesChanged.push(".codebyplan/shipment.json");
6114
6260
  const vendorJson = {};
6115
- await writeFile11(
6116
- join20(projectPath, ".codebyplan", "vendor.json"),
6261
+ await writeFile12(
6262
+ join21(projectPath, ".codebyplan", "vendor.json"),
6117
6263
  JSON.stringify(vendorJson, null, 2) + "\n",
6118
6264
  "utf-8"
6119
6265
  );
6120
6266
  filesChanged.push(".codebyplan/vendor.json");
6121
6267
  const e2eJson = {};
6122
- await writeFile11(
6123
- join20(projectPath, ".codebyplan", "e2e.json"),
6268
+ await writeFile12(
6269
+ join21(projectPath, ".codebyplan", "e2e.json"),
6124
6270
  JSON.stringify(e2eJson, null, 2) + "\n",
6125
6271
  "utf-8"
6126
6272
  );
6127
6273
  filesChanged.push(".codebyplan/e2e.json");
6128
6274
  const eslintJson = {};
6129
- await writeFile11(
6130
- join20(projectPath, ".codebyplan", "eslint.json"),
6275
+ await writeFile12(
6276
+ join21(projectPath, ".codebyplan", "eslint.json"),
6131
6277
  JSON.stringify(eslintJson, null, 2) + "\n",
6132
6278
  "utf-8"
6133
6279
  );
6134
6280
  filesChanged.push(".codebyplan/eslint.json");
6135
6281
  if (!deviceWrittenByHelper) {
6136
- await writeFile11(
6137
- join20(projectPath, ".codebyplan", "device.local.json"),
6282
+ await writeFile12(
6283
+ join21(projectPath, ".codebyplan", "device.local.json"),
6138
6284
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
6139
6285
  "utf-8"
6140
6286
  );
@@ -6146,9 +6292,9 @@ async function runLocalMigration(projectPath) {
6146
6292
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
6147
6293
  );
6148
6294
  }
6149
- const gitignorePath = join20(projectPath, ".gitignore");
6295
+ const gitignorePath = join21(projectPath, ".gitignore");
6150
6296
  try {
6151
- const gitignoreContent = await readFile14(gitignorePath, "utf-8");
6297
+ const gitignoreContent = await readFile15(gitignorePath, "utf-8");
6152
6298
  const legacyLine = ".codebyplan.local.json";
6153
6299
  const newLine = ".codebyplan/device.local.json";
6154
6300
  const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
@@ -6167,7 +6313,7 @@ async function runLocalMigration(projectPath) {
6167
6313
  updated = gitignoreContent;
6168
6314
  }
6169
6315
  if (updated !== gitignoreContent) {
6170
- await writeFile11(gitignorePath, updated, "utf-8");
6316
+ await writeFile12(gitignorePath, updated, "utf-8");
6171
6317
  filesChanged.push(".gitignore");
6172
6318
  }
6173
6319
  } catch {
@@ -6207,8 +6353,8 @@ __export(config_exports, {
6207
6353
  readVendorConfig: () => readVendorConfig,
6208
6354
  runConfig: () => runConfig
6209
6355
  });
6210
- import { mkdir as mkdir6, readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
6211
- import { join as join21 } from "node:path";
6356
+ import { mkdir as mkdir7, readFile as readFile16, writeFile as writeFile13 } from "node:fs/promises";
6357
+ import { join as join22 } from "node:path";
6212
6358
  async function runConfig() {
6213
6359
  const flags = parseFlags(3);
6214
6360
  const dryRun = hasFlag("dry-run", 3);
@@ -6241,7 +6387,7 @@ async function runConfig() {
6241
6387
  console.log("\n Config complete.\n");
6242
6388
  }
6243
6389
  async function syncConfigToFile(repoId, projectPath, dryRun) {
6244
- const codebyplanDir = join21(projectPath, ".codebyplan");
6390
+ const codebyplanDir = join22(projectPath, ".codebyplan");
6245
6391
  let resolvedWorktreeId;
6246
6392
  try {
6247
6393
  const deviceId = await getOrCreateDeviceId(projectPath);
@@ -6372,7 +6518,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
6372
6518
  console.log(" Config would be updated (dry-run).");
6373
6519
  return;
6374
6520
  }
6375
- await mkdir6(codebyplanDir, { recursive: true });
6521
+ await mkdir7(codebyplanDir, { recursive: true });
6376
6522
  const files = [
6377
6523
  { name: "repo.json", payload: repoPayload },
6378
6524
  { name: "server.json", payload: serverPayload },
@@ -6384,16 +6530,16 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
6384
6530
  ];
6385
6531
  let anyUpdated = false;
6386
6532
  for (const { name, payload, createOnly } of files) {
6387
- const filePath = join21(codebyplanDir, name);
6533
+ const filePath = join22(codebyplanDir, name);
6388
6534
  const newJson = JSON.stringify(payload, null, 2) + "\n";
6389
6535
  let currentJson = "";
6390
6536
  try {
6391
- currentJson = await readFile15(filePath, "utf-8");
6537
+ currentJson = await readFile16(filePath, "utf-8");
6392
6538
  } catch {
6393
6539
  }
6394
6540
  if (createOnly && currentJson !== "") continue;
6395
6541
  if (currentJson === newJson) continue;
6396
- await writeFile12(filePath, newJson, "utf-8");
6542
+ await writeFile13(filePath, newJson, "utf-8");
6397
6543
  console.log(` Updated .codebyplan/${name}`);
6398
6544
  anyUpdated = true;
6399
6545
  }
@@ -6403,8 +6549,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
6403
6549
  }
6404
6550
  async function readRepoConfig(projectPath) {
6405
6551
  try {
6406
- const raw = await readFile15(
6407
- join21(projectPath, ".codebyplan", "repo.json"),
6552
+ const raw = await readFile16(
6553
+ join22(projectPath, ".codebyplan", "repo.json"),
6408
6554
  "utf-8"
6409
6555
  );
6410
6556
  return JSON.parse(raw);
@@ -6414,8 +6560,8 @@ async function readRepoConfig(projectPath) {
6414
6560
  }
6415
6561
  async function readServerConfig(projectPath) {
6416
6562
  try {
6417
- const raw = await readFile15(
6418
- join21(projectPath, ".codebyplan", "server.json"),
6563
+ const raw = await readFile16(
6564
+ join22(projectPath, ".codebyplan", "server.json"),
6419
6565
  "utf-8"
6420
6566
  );
6421
6567
  return JSON.parse(raw);
@@ -6425,8 +6571,8 @@ async function readServerConfig(projectPath) {
6425
6571
  }
6426
6572
  async function readGitConfig(projectPath) {
6427
6573
  try {
6428
- const raw = await readFile15(
6429
- join21(projectPath, ".codebyplan", "git.json"),
6574
+ const raw = await readFile16(
6575
+ join22(projectPath, ".codebyplan", "git.json"),
6430
6576
  "utf-8"
6431
6577
  );
6432
6578
  return JSON.parse(raw);
@@ -6436,8 +6582,8 @@ async function readGitConfig(projectPath) {
6436
6582
  }
6437
6583
  async function readShipmentConfig(projectPath) {
6438
6584
  try {
6439
- const raw = await readFile15(
6440
- join21(projectPath, ".codebyplan", "shipment.json"),
6585
+ const raw = await readFile16(
6586
+ join22(projectPath, ".codebyplan", "shipment.json"),
6441
6587
  "utf-8"
6442
6588
  );
6443
6589
  return JSON.parse(raw);
@@ -6447,8 +6593,8 @@ async function readShipmentConfig(projectPath) {
6447
6593
  }
6448
6594
  async function readVendorConfig(projectPath) {
6449
6595
  try {
6450
- const raw = await readFile15(
6451
- join21(projectPath, ".codebyplan", "vendor.json"),
6596
+ const raw = await readFile16(
6597
+ join22(projectPath, ".codebyplan", "vendor.json"),
6452
6598
  "utf-8"
6453
6599
  );
6454
6600
  return JSON.parse(raw);
@@ -6458,8 +6604,8 @@ async function readVendorConfig(projectPath) {
6458
6604
  }
6459
6605
  async function readE2eConfig(projectPath) {
6460
6606
  try {
6461
- const raw = await readFile15(
6462
- join21(projectPath, ".codebyplan", "e2e.json"),
6607
+ const raw = await readFile16(
6608
+ join22(projectPath, ".codebyplan", "e2e.json"),
6463
6609
  "utf-8"
6464
6610
  );
6465
6611
  return JSON.parse(raw);
@@ -6515,14 +6661,14 @@ var init_server_detect = __esm({
6515
6661
  });
6516
6662
 
6517
6663
  // src/lib/port-verify.ts
6518
- import { readFile as readFile16 } from "node:fs/promises";
6664
+ import { readFile as readFile17 } from "node:fs/promises";
6519
6665
  async function verifyPorts(projectPath, portAllocations) {
6520
6666
  const mismatches = [];
6521
6667
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
6522
6668
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
6523
6669
  for (const pkgPath of packageJsonPaths) {
6524
6670
  try {
6525
- const raw = await readFile16(pkgPath, "utf-8");
6671
+ const raw = await readFile17(pkgPath, "utf-8");
6526
6672
  const pkg = JSON.parse(raw);
6527
6673
  const scriptPort = detectPortFromScripts(pkg);
6528
6674
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -6585,7 +6731,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
6585
6731
  }
6586
6732
  let pkg;
6587
6733
  try {
6588
- const raw = await readFile16(`${app.absPath}/package.json`, "utf-8");
6734
+ const raw = await readFile17(`${app.absPath}/package.json`, "utf-8");
6589
6735
  pkg = JSON.parse(raw);
6590
6736
  } catch {
6591
6737
  continue;
@@ -7852,6 +7998,11 @@ void (async () => {
7852
7998
  --node Set statusline renderer to node after install/update
7853
7999
  --python Set statusline renderer to python after install/update
7854
8000
 
8001
+ Resolve-worktree options:
8002
+ --json Structured JSON output instead of bare UUID
8003
+ --fallback-from-branch Fallback resolver: filter by (device_id, branch) client-side
8004
+ --cache Resolve and write worktree id to .codebyplan/worktree.local.json
8005
+
7855
8006
  MCP Server:
7856
8007
  Claude Code connects to CodeByPlan via remote MCP:
7857
8008
  URL: https://mcp.codebyplan.com/mcp
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.9",
3
+ "version": "1.13.10",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,14 @@
12
12
  # - Web-UI flag (app_file_approval_by_user) consumption + reset
13
13
  # - Writes both PATCH /api/rounds/${ROUND_ID} and PATCH /api/tasks/${TASK_ID}
14
14
  #
15
+ # Caller worktree identity:
16
+ # The CLI auto-resolves caller_worktree_id (override flag → cache →
17
+ # in-process tuple API) and hard-fails with exit 1 if it cannot resolve
18
+ # on the write path. This hook resolves the worktree id before invoking the
19
+ # CLI and passes it via --caller-worktree-id so the server can honor the
20
+ # feat-worktree lock. The hook itself stays non-fatal (exits 0) and surfaces
21
+ # the CLI's stderr output to the user.
22
+ #
15
23
  # Flags:
16
24
  # --dry-run Pass through to CLI (prints merged payload, no API writes).
17
25
  # Used by fixture-based smoke tests.
@@ -67,11 +75,19 @@ if [ -z "$TASK_ID" ]; then
67
75
  exit 0
68
76
  fi
69
77
 
78
+ # Resolve worktree id before invoking the CLI so the server can honor the
79
+ # feat-worktree lock. On miss (unregistered worktree) the CLI falls back to
80
+ # its in-process resolve and hard-fails with guidance if still unresolved.
81
+ WORKTREE_ID=$(npx codebyplan resolve-worktree 2>/dev/null)
82
+
70
83
  # Delegate to the codebyplan CLI (single source of truth for merge semantics)
71
84
  CMD_ARGS=("round" "sync-approvals" "--round-id" "$ROUND_ID" "--task-id" "$TASK_ID")
85
+ if [ -n "$WORKTREE_ID" ]; then
86
+ CMD_ARGS+=("--caller-worktree-id" "$WORKTREE_ID")
87
+ fi
72
88
  [ "$DRY_RUN" = "true" ] && CMD_ARGS+=("--dry-run")
73
89
 
74
- if npx codebyplan "${CMD_ARGS[@]}" 2>&1; then
90
+ if npx codebyplan "${CMD_ARGS[@]}"; then
75
91
  echo "cbp-mcp-round-sync: synced via CLI for round ${ROUND_ID}" >&2
76
92
  else
77
93
  echo "cbp-mcp-round-sync: CLI sync failed for round ${ROUND_ID} (non-fatal)" >&2
@@ -196,7 +196,7 @@ echo '{}' > "$WORKTREE_PATH/.codebyplan/shipment.json"
196
196
  echo '{}' > "$WORKTREE_PATH/.codebyplan/vendor.json"
197
197
  ```
198
198
 
199
- The `.codebyplan/device.local.json` file is created by `npx codebyplan setup` on the device (gitignored). The `worktree_id` is never persisted in any of these files it is resolved at runtime via `npx codebyplan resolve-worktree` based on the device_id, filesystem path, and current git branch.
199
+ The `.codebyplan/device.local.json` file is created by `npx codebyplan setup` on the device (gitignored). The `worktree_id` is never COMMITTED; it may be cached per-device in the gitignored `.codebyplan/worktree.local.json` (branch-keyed, re-derivable via `codebyplan resolve-worktree --cache`), otherwise resolved at runtime from the `(device_id, repo path, branch)` tuple via `npx codebyplan resolve-worktree`.
200
200
 
201
201
  No need to mark as `skip-worktree` — the committed files are merge-safe per CHK-108 and CHK-120.
202
202
 
@@ -69,7 +69,20 @@ Run:
69
69
  npx codebyplan round sync-approvals --round-id <round_id> --task-id <task_id>
70
70
  ```
71
71
 
72
- The CLI auto-resolves the worktree id, parses `git status --short`, merges drift + staging + web-UI flag, and writes both round and task.
72
+ The CLI auto-resolves the caller worktree id with the following precedence:
73
+ 1. `--caller-worktree-id <uuid>` override (if passed — skips all resolution)
74
+ 2. Per-device branch-keyed cache (`.codebyplan/worktree.local.json`)
75
+ 3. In-process tuple API call: `POST /worktrees/resolve` using `(device_id, repo_path, branch)`
76
+
77
+ On the write path (non `--dry-run`), if the worktree id cannot be resolved the CLI **hard-fails with exit 1** and prints an actionable message. To pre-populate the cache:
78
+
79
+ ```
80
+ npx codebyplan resolve-worktree --cache
81
+ ```
82
+
83
+ If this worktree is not yet registered, run `npx codebyplan setup` first, then re-run `/cbp-round-update`.
84
+
85
+ The CLI parses `git status --short`, merges drift + staging + web-UI flag, and writes both round and task (forwarding `caller_worktree_id` on both writes so the server honors the feat-worktree lock).
73
86
 
74
87
  Read the stdout JSON: `{ added, stale_marked, reactivated, total_files }`.
75
88