codebyplan 1.13.8 → 1.13.9

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.8";
17
+ VERSION = "1.13.9";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -1443,8 +1443,8 @@ async function runSetup() {
1443
1443
  const deviceId = await getOrCreateDeviceId(projectPath);
1444
1444
  let branch = "main";
1445
1445
  try {
1446
- const { execSync: execSync7 } = await import("node:child_process");
1447
- branch = execSync7("git symbolic-ref --short HEAD", {
1446
+ const { execSync: execSync8 } = await import("node:child_process");
1447
+ branch = execSync8("git symbolic-ref --short HEAD", {
1448
1448
  cwd: projectPath,
1449
1449
  encoding: "utf-8"
1450
1450
  }).trim();
@@ -3064,9 +3064,9 @@ async function eslintInit(repoId, projectPath) {
3064
3064
  Install ${missingPkgs.length} missing packages? [Y/n] `
3065
3065
  );
3066
3066
  if (confirmed) {
3067
- const { execSync: execSync7 } = await import("node:child_process");
3067
+ const { execSync: execSync8 } = await import("node:child_process");
3068
3068
  try {
3069
- execSync7(installCmd, { cwd: projectPath, stdio: "inherit" });
3069
+ execSync8(installCmd, { cwd: projectPath, stdio: "inherit" });
3070
3070
  console.log(" Packages installed.\n");
3071
3071
  } catch (err) {
3072
3072
  console.error(
@@ -5735,12 +5735,181 @@ var init_resolve_worktree2 = __esm({
5735
5735
  }
5736
5736
  });
5737
5737
 
5738
+ // src/cli/version-status.ts
5739
+ var version_status_exports = {};
5740
+ __export(version_status_exports, {
5741
+ runVersionStatus: () => runVersionStatus
5742
+ });
5743
+ import { execFileSync, execSync as execSync6 } from "node:child_process";
5744
+ import { existsSync as existsSync4, readFileSync as readFileSync5 } from "node:fs";
5745
+ import { dirname as dirname7, join as join19 } from "node:path";
5746
+ function fetchLatestVersion() {
5747
+ try {
5748
+ return execFileSync("npm", ["view", "codebyplan", "version"], {
5749
+ encoding: "utf-8",
5750
+ stdio: ["pipe", "pipe", "pipe"]
5751
+ }).trim() || null;
5752
+ } catch {
5753
+ return null;
5754
+ }
5755
+ }
5756
+ function resolveGitRoot() {
5757
+ try {
5758
+ return execSync6("git rev-parse --show-toplevel", {
5759
+ encoding: "utf-8"
5760
+ }).trim();
5761
+ } catch {
5762
+ return null;
5763
+ }
5764
+ }
5765
+ function resolveCurrentBranch() {
5766
+ try {
5767
+ return execSync6("git symbolic-ref --short HEAD", {
5768
+ encoding: "utf-8"
5769
+ }).trim();
5770
+ } catch {
5771
+ try {
5772
+ return execSync6("git rev-parse --abbrev-ref HEAD", {
5773
+ encoding: "utf-8"
5774
+ }).trim();
5775
+ } catch {
5776
+ return "";
5777
+ }
5778
+ }
5779
+ }
5780
+ function detectPackageManager2(gitRoot) {
5781
+ let dir = process.cwd();
5782
+ const stopAt = gitRoot ?? null;
5783
+ 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";
5787
+ if (stopAt !== null && dir === stopAt) break;
5788
+ const parent = dirname7(dir);
5789
+ if (parent === dir) break;
5790
+ dir = parent;
5791
+ }
5792
+ return "npm";
5793
+ }
5794
+ function buildInstallCommand2(pm) {
5795
+ switch (pm) {
5796
+ case "pnpm":
5797
+ return "pnpm add codebyplan@latest";
5798
+ case "yarn":
5799
+ return "yarn add codebyplan@latest";
5800
+ case "npm":
5801
+ return "npm install codebyplan@latest";
5802
+ }
5803
+ }
5804
+ async function resolveGuard(gitRoot, currentBranch) {
5805
+ if (gitRoot !== null) {
5806
+ const canonicalPkgPath = join19(
5807
+ gitRoot,
5808
+ "packages",
5809
+ "codebyplan-package",
5810
+ "package.json"
5811
+ );
5812
+ try {
5813
+ if (existsSync4(canonicalPkgPath)) {
5814
+ const raw = readFileSync5(canonicalPkgPath, "utf-8");
5815
+ const parsed = JSON.parse(raw);
5816
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && parsed["name"] === "codebyplan") {
5817
+ return { guarded: true, guardReason: "canonical_source" };
5818
+ }
5819
+ }
5820
+ } catch {
5821
+ }
5822
+ }
5823
+ let protectedBranches = [];
5824
+ try {
5825
+ const found = await findCodebyplanConfig(process.cwd());
5826
+ if (found) {
5827
+ let gitJsonPath;
5828
+ if (found.path.endsWith("/repo.json")) {
5829
+ const dir = found.path.slice(0, found.path.lastIndexOf("/"));
5830
+ gitJsonPath = join19(dir, "git.json");
5831
+ } else {
5832
+ const legacyDir = found.path.slice(0, found.path.lastIndexOf("/"));
5833
+ gitJsonPath = join19(legacyDir, ".codebyplan", "git.json");
5834
+ }
5835
+ const raw = readFileSync5(gitJsonPath, "utf-8");
5836
+ const parsed = JSON.parse(raw);
5837
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
5838
+ const bc = parsed["branch_config"];
5839
+ if (typeof bc === "object" && bc !== null && !Array.isArray(bc)) {
5840
+ const protectedField = bc["protected"];
5841
+ if (Array.isArray(protectedField)) {
5842
+ protectedBranches = protectedField.filter(
5843
+ (v) => typeof v === "string"
5844
+ );
5845
+ }
5846
+ }
5847
+ }
5848
+ }
5849
+ } catch {
5850
+ protectedBranches = [];
5851
+ }
5852
+ const isProtected = currentBranch === "main" || currentBranch !== "" && protectedBranches.includes(currentBranch);
5853
+ if (isProtected) {
5854
+ return { guarded: true, guardReason: "protected_branch" };
5855
+ }
5856
+ return { guarded: false, guardReason: null };
5857
+ }
5858
+ async function runVersionStatus() {
5859
+ try {
5860
+ const installed = VERSION;
5861
+ const latest = fetchLatestVersion();
5862
+ const newer = latest !== null && compareSemver(latest, installed) > 0;
5863
+ const gitRoot = resolveGitRoot();
5864
+ const pm = detectPackageManager2(gitRoot);
5865
+ const installCommand = buildInstallCommand2(pm);
5866
+ const currentBranch = resolveCurrentBranch();
5867
+ const { guarded, guardReason } = await resolveGuard(gitRoot, currentBranch);
5868
+ const result = {
5869
+ installed,
5870
+ latest,
5871
+ newer,
5872
+ packageManager: pm,
5873
+ installCommand,
5874
+ guarded,
5875
+ guardReason
5876
+ };
5877
+ process.stdout.write(JSON.stringify(result) + "\n");
5878
+ process.exit(0);
5879
+ } catch (err) {
5880
+ if (err instanceof ProcessExitSignal) throw err;
5881
+ process.stdout.write(JSON.stringify(FAIL_SAFE) + "\n");
5882
+ process.exit(0);
5883
+ }
5884
+ }
5885
+ var FAIL_SAFE;
5886
+ var init_version_status = __esm({
5887
+ "src/cli/version-status.ts"() {
5888
+ "use strict";
5889
+ init_version();
5890
+ init_bump();
5891
+ init_flags();
5892
+ init_process_exit_signal();
5893
+ FAIL_SAFE = {
5894
+ installed: VERSION,
5895
+ latest: null,
5896
+ newer: false,
5897
+ packageManager: "npm",
5898
+ installCommand: "npm install codebyplan@latest",
5899
+ guarded: true,
5900
+ // Detection failed entirely — guard conservatively with an explicit reason
5901
+ // so consumers never see guarded:true paired with a null reason.
5902
+ guardReason: "unknown"
5903
+ };
5904
+ }
5905
+ });
5906
+
5738
5907
  // src/cli/cmux-sync.ts
5739
5908
  var cmux_sync_exports = {};
5740
5909
  __export(cmux_sync_exports, {
5741
5910
  runCmuxSync: () => runCmuxSync
5742
5911
  });
5743
- import { execSync as execSync6, execFileSync } from "node:child_process";
5912
+ import { execSync as execSync7, execFileSync as execFileSync2 } from "node:child_process";
5744
5913
  import { basename } from "node:path";
5745
5914
  async function runCmuxSync() {
5746
5915
  try {
@@ -5750,14 +5919,14 @@ async function runCmuxSync() {
5750
5919
  const bin = process.env.CMUX_BUNDLED_CLI_PATH || process.env.CMUX_CLAUDE_HOOK_CMUX_BIN || "cmux";
5751
5920
  let branch = "";
5752
5921
  try {
5753
- branch = execSync6("git rev-parse --abbrev-ref HEAD", {
5922
+ branch = execSync7("git rev-parse --abbrev-ref HEAD", {
5754
5923
  encoding: "utf8"
5755
5924
  }).trim();
5756
5925
  } catch {
5757
5926
  }
5758
5927
  let folder = "";
5759
5928
  try {
5760
- const toplevel = execSync6("git rev-parse --show-toplevel", {
5929
+ const toplevel = execSync7("git rev-parse --show-toplevel", {
5761
5930
  encoding: "utf8"
5762
5931
  }).trim();
5763
5932
  folder = basename(toplevel);
@@ -5765,7 +5934,7 @@ async function runCmuxSync() {
5765
5934
  }
5766
5935
  if (branch) {
5767
5936
  try {
5768
- execFileSync(bin, [
5937
+ execFileSync2(bin, [
5769
5938
  "workspace-action",
5770
5939
  "--action",
5771
5940
  "rename",
@@ -5777,7 +5946,7 @@ async function runCmuxSync() {
5777
5946
  }
5778
5947
  if (folder) {
5779
5948
  try {
5780
- execFileSync(bin, [
5949
+ execFileSync2(bin, [
5781
5950
  "workspace-action",
5782
5951
  "--action",
5783
5952
  "set-description",
@@ -5802,18 +5971,18 @@ var init_cmux_sync = __esm({
5802
5971
 
5803
5972
  // src/lib/migrate-local-config.ts
5804
5973
  import { mkdir as mkdir5, readFile as readFile14, unlink as unlink2, writeFile as writeFile11 } from "node:fs/promises";
5805
- import { join as join19 } from "node:path";
5974
+ import { join as join20 } from "node:path";
5806
5975
  function legacySharedPath(projectPath) {
5807
- return join19(projectPath, ".codebyplan.json");
5976
+ return join20(projectPath, ".codebyplan.json");
5808
5977
  }
5809
5978
  function legacyLocalPath(projectPath) {
5810
- return join19(projectPath, ".codebyplan.local.json");
5979
+ return join20(projectPath, ".codebyplan.local.json");
5811
5980
  }
5812
5981
  function newDirPath(projectPath) {
5813
- return join19(projectPath, ".codebyplan");
5982
+ return join20(projectPath, ".codebyplan");
5814
5983
  }
5815
5984
  function sentinelPath(projectPath) {
5816
- return join19(projectPath, ".codebyplan", "repo.json");
5985
+ return join20(projectPath, ".codebyplan", "repo.json");
5817
5986
  }
5818
5987
  async function statSafe(p) {
5819
5988
  const { stat: stat2 } = await import("node:fs/promises");
@@ -5907,7 +6076,7 @@ async function runLocalMigration(projectPath) {
5907
6076
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
5908
6077
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
5909
6078
  await writeFile11(
5910
- join19(projectPath, ".codebyplan", "repo.json"),
6079
+ join20(projectPath, ".codebyplan", "repo.json"),
5911
6080
  JSON.stringify(repoJson, null, 2) + "\n",
5912
6081
  "utf-8"
5913
6082
  );
@@ -5920,7 +6089,7 @@ async function runLocalMigration(projectPath) {
5920
6089
  if ("port_allocations" in cfg)
5921
6090
  serverJson.port_allocations = cfg.port_allocations;
5922
6091
  await writeFile11(
5923
- join19(projectPath, ".codebyplan", "server.json"),
6092
+ join20(projectPath, ".codebyplan", "server.json"),
5924
6093
  JSON.stringify(serverJson, null, 2) + "\n",
5925
6094
  "utf-8"
5926
6095
  );
@@ -5929,7 +6098,7 @@ async function runLocalMigration(projectPath) {
5929
6098
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
5930
6099
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
5931
6100
  await writeFile11(
5932
- join19(projectPath, ".codebyplan", "git.json"),
6101
+ join20(projectPath, ".codebyplan", "git.json"),
5933
6102
  JSON.stringify(gitJson, null, 2) + "\n",
5934
6103
  "utf-8"
5935
6104
  );
@@ -5937,35 +6106,35 @@ async function runLocalMigration(projectPath) {
5937
6106
  const shipmentJson = {};
5938
6107
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
5939
6108
  await writeFile11(
5940
- join19(projectPath, ".codebyplan", "shipment.json"),
6109
+ join20(projectPath, ".codebyplan", "shipment.json"),
5941
6110
  JSON.stringify(shipmentJson, null, 2) + "\n",
5942
6111
  "utf-8"
5943
6112
  );
5944
6113
  filesChanged.push(".codebyplan/shipment.json");
5945
6114
  const vendorJson = {};
5946
6115
  await writeFile11(
5947
- join19(projectPath, ".codebyplan", "vendor.json"),
6116
+ join20(projectPath, ".codebyplan", "vendor.json"),
5948
6117
  JSON.stringify(vendorJson, null, 2) + "\n",
5949
6118
  "utf-8"
5950
6119
  );
5951
6120
  filesChanged.push(".codebyplan/vendor.json");
5952
6121
  const e2eJson = {};
5953
6122
  await writeFile11(
5954
- join19(projectPath, ".codebyplan", "e2e.json"),
6123
+ join20(projectPath, ".codebyplan", "e2e.json"),
5955
6124
  JSON.stringify(e2eJson, null, 2) + "\n",
5956
6125
  "utf-8"
5957
6126
  );
5958
6127
  filesChanged.push(".codebyplan/e2e.json");
5959
6128
  const eslintJson = {};
5960
6129
  await writeFile11(
5961
- join19(projectPath, ".codebyplan", "eslint.json"),
6130
+ join20(projectPath, ".codebyplan", "eslint.json"),
5962
6131
  JSON.stringify(eslintJson, null, 2) + "\n",
5963
6132
  "utf-8"
5964
6133
  );
5965
6134
  filesChanged.push(".codebyplan/eslint.json");
5966
6135
  if (!deviceWrittenByHelper) {
5967
6136
  await writeFile11(
5968
- join19(projectPath, ".codebyplan", "device.local.json"),
6137
+ join20(projectPath, ".codebyplan", "device.local.json"),
5969
6138
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
5970
6139
  "utf-8"
5971
6140
  );
@@ -5977,7 +6146,7 @@ async function runLocalMigration(projectPath) {
5977
6146
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
5978
6147
  );
5979
6148
  }
5980
- const gitignorePath = join19(projectPath, ".gitignore");
6149
+ const gitignorePath = join20(projectPath, ".gitignore");
5981
6150
  try {
5982
6151
  const gitignoreContent = await readFile14(gitignorePath, "utf-8");
5983
6152
  const legacyLine = ".codebyplan.local.json";
@@ -6039,7 +6208,7 @@ __export(config_exports, {
6039
6208
  runConfig: () => runConfig
6040
6209
  });
6041
6210
  import { mkdir as mkdir6, readFile as readFile15, writeFile as writeFile12 } from "node:fs/promises";
6042
- import { join as join20 } from "node:path";
6211
+ import { join as join21 } from "node:path";
6043
6212
  async function runConfig() {
6044
6213
  const flags = parseFlags(3);
6045
6214
  const dryRun = hasFlag("dry-run", 3);
@@ -6072,14 +6241,14 @@ async function runConfig() {
6072
6241
  console.log("\n Config complete.\n");
6073
6242
  }
6074
6243
  async function syncConfigToFile(repoId, projectPath, dryRun) {
6075
- const codebyplanDir = join20(projectPath, ".codebyplan");
6244
+ const codebyplanDir = join21(projectPath, ".codebyplan");
6076
6245
  let resolvedWorktreeId;
6077
6246
  try {
6078
6247
  const deviceId = await getOrCreateDeviceId(projectPath);
6079
6248
  let branch = "main";
6080
6249
  try {
6081
- const { execSync: execSync7 } = await import("node:child_process");
6082
- branch = execSync7("git symbolic-ref --short HEAD", {
6250
+ const { execSync: execSync8 } = await import("node:child_process");
6251
+ branch = execSync8("git symbolic-ref --short HEAD", {
6083
6252
  cwd: projectPath,
6084
6253
  encoding: "utf-8"
6085
6254
  }).trim();
@@ -6215,7 +6384,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
6215
6384
  ];
6216
6385
  let anyUpdated = false;
6217
6386
  for (const { name, payload, createOnly } of files) {
6218
- const filePath = join20(codebyplanDir, name);
6387
+ const filePath = join21(codebyplanDir, name);
6219
6388
  const newJson = JSON.stringify(payload, null, 2) + "\n";
6220
6389
  let currentJson = "";
6221
6390
  try {
@@ -6235,7 +6404,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
6235
6404
  async function readRepoConfig(projectPath) {
6236
6405
  try {
6237
6406
  const raw = await readFile15(
6238
- join20(projectPath, ".codebyplan", "repo.json"),
6407
+ join21(projectPath, ".codebyplan", "repo.json"),
6239
6408
  "utf-8"
6240
6409
  );
6241
6410
  return JSON.parse(raw);
@@ -6246,7 +6415,7 @@ async function readRepoConfig(projectPath) {
6246
6415
  async function readServerConfig(projectPath) {
6247
6416
  try {
6248
6417
  const raw = await readFile15(
6249
- join20(projectPath, ".codebyplan", "server.json"),
6418
+ join21(projectPath, ".codebyplan", "server.json"),
6250
6419
  "utf-8"
6251
6420
  );
6252
6421
  return JSON.parse(raw);
@@ -6257,7 +6426,7 @@ async function readServerConfig(projectPath) {
6257
6426
  async function readGitConfig(projectPath) {
6258
6427
  try {
6259
6428
  const raw = await readFile15(
6260
- join20(projectPath, ".codebyplan", "git.json"),
6429
+ join21(projectPath, ".codebyplan", "git.json"),
6261
6430
  "utf-8"
6262
6431
  );
6263
6432
  return JSON.parse(raw);
@@ -6268,7 +6437,7 @@ async function readGitConfig(projectPath) {
6268
6437
  async function readShipmentConfig(projectPath) {
6269
6438
  try {
6270
6439
  const raw = await readFile15(
6271
- join20(projectPath, ".codebyplan", "shipment.json"),
6440
+ join21(projectPath, ".codebyplan", "shipment.json"),
6272
6441
  "utf-8"
6273
6442
  );
6274
6443
  return JSON.parse(raw);
@@ -6279,7 +6448,7 @@ async function readShipmentConfig(projectPath) {
6279
6448
  async function readVendorConfig(projectPath) {
6280
6449
  try {
6281
6450
  const raw = await readFile15(
6282
- join20(projectPath, ".codebyplan", "vendor.json"),
6451
+ join21(projectPath, ".codebyplan", "vendor.json"),
6283
6452
  "utf-8"
6284
6453
  );
6285
6454
  return JSON.parse(raw);
@@ -6290,7 +6459,7 @@ async function readVendorConfig(projectPath) {
6290
6459
  async function readE2eConfig(projectPath) {
6291
6460
  try {
6292
6461
  const raw = await readFile15(
6293
- join20(projectPath, ".codebyplan", "e2e.json"),
6462
+ join21(projectPath, ".codebyplan", "e2e.json"),
6294
6463
  "utf-8"
6295
6464
  );
6296
6465
  return JSON.parse(raw);
@@ -6562,7 +6731,7 @@ __export(tech_stack_exports, {
6562
6731
  runFullTechStack: () => runFullTechStack,
6563
6732
  runTechStack: () => runTechStack
6564
6733
  });
6565
- import { existsSync as existsSync4 } from "node:fs";
6734
+ import { existsSync as existsSync5 } from "node:fs";
6566
6735
  async function runTechStack() {
6567
6736
  const flags = parseFlags(3);
6568
6737
  const dryRun = hasFlag("dry-run", 3);
@@ -6624,10 +6793,10 @@ async function runTechStack() {
6624
6793
  );
6625
6794
  }
6626
6795
  try {
6627
- const { execSync: execSync7 } = await import("node:child_process");
6796
+ const { execSync: execSync8 } = await import("node:child_process");
6628
6797
  let branch = "main";
6629
6798
  try {
6630
- branch = execSync7("git symbolic-ref --short HEAD", {
6799
+ branch = execSync8("git symbolic-ref --short HEAD", {
6631
6800
  cwd: projectPath,
6632
6801
  encoding: "utf-8"
6633
6802
  }).trim();
@@ -6735,7 +6904,7 @@ async function runFullTechStack(dryRun) {
6735
6904
  continue;
6736
6905
  }
6737
6906
  const localWorktrees = worktrees.filter(
6738
- (wt) => wt.path ? existsSync4(wt.path) : false
6907
+ (wt) => wt.path ? existsSync5(wt.path) : false
6739
6908
  );
6740
6909
  if (localWorktrees.length === 0) {
6741
6910
  console.log(` skipping ${repo.name} \u2014 no local worktree on this device`);
@@ -7418,13 +7587,13 @@ var init_uninstall = __esm({
7418
7587
 
7419
7588
  // src/index.ts
7420
7589
  init_version();
7421
- import { readFileSync as readFileSync7 } from "node:fs";
7590
+ import { readFileSync as readFileSync8 } from "node:fs";
7422
7591
  import { resolve as resolve6 } from "node:path";
7423
7592
  void (async () => {
7424
7593
  if (!process.env.CODEBYPLAN_API_KEY) {
7425
7594
  try {
7426
7595
  const envPath = resolve6(process.cwd(), ".env.local");
7427
- const content = readFileSync7(envPath, "utf-8");
7596
+ const content = readFileSync8(envPath, "utf-8");
7428
7597
  for (const line of content.split("\n")) {
7429
7598
  const trimmed = line.trim();
7430
7599
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -7536,6 +7705,11 @@ void (async () => {
7536
7705
  await runResolveWorktree2();
7537
7706
  process.exit(0);
7538
7707
  }
7708
+ if (arg === "version-status") {
7709
+ const { runVersionStatus: runVersionStatus2 } = await Promise.resolve().then(() => (init_version_status(), version_status_exports));
7710
+ await runVersionStatus2();
7711
+ process.exit(0);
7712
+ }
7539
7713
  if (arg === "cmux-sync") {
7540
7714
  const { runCmuxSync: runCmuxSync2 } = await Promise.resolve().then(() => (init_cmux_sync(), cmux_sync_exports));
7541
7715
  await runCmuxSync2();
@@ -7639,6 +7813,7 @@ void (async () => {
7639
7813
  codebyplan claude Claude asset management (install/update/uninstall)
7640
7814
  codebyplan statusline Show or set the statusline renderer (bash/node/python)
7641
7815
  codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
7816
+ codebyplan version-status Report installed vs latest version + update guard (JSON)
7642
7817
  codebyplan cmux-sync Sync cmux workspace title/description to current git branch and repo folder
7643
7818
  codebyplan help Show this help message
7644
7819
  codebyplan --version Print version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.8",
3
+ "version": "1.13.9",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -181,6 +181,8 @@
181
181
  "Bash(npx codebyplan resolve-worktree:*)",
182
182
  "Bash(codebyplan cmux-sync:*)",
183
183
  "Bash(npx codebyplan cmux-sync:*)",
184
+ "Bash(codebyplan version-status:*)",
185
+ "Bash(npx codebyplan version-status:*)",
184
186
  "Bash(codebyplan statusline:*)",
185
187
  "Bash(npx codebyplan statusline:*)",
186
188
  "Bash(codebyplan ports:*)",
@@ -25,7 +25,7 @@ Always write a session log for this session — **even if empty**. `/cbp-session
25
25
 
26
26
  ### Step 1.3: Capture Handoff Snapshot
27
27
 
28
- Snapshot the current next-action so the next `/cbp-session-start` (Step 4.5) can auto-resume. Per `.claude/rules/session-resume.md` write-path contract.
28
+ Snapshot the current next-action so the next `/cbp-session-start` (Step 4.5) can auto-resume. The handoff write-path + payload shape are specified inline here and in `/cbp-session-start` Step 4.5 (freshness gate).
29
29
 
30
30
  1. Call MCP `get_next_action({ repo_id, worktree_id })`.
31
31
  2. If the returned `command` is non-empty (active work in flight):
@@ -60,7 +60,8 @@ Same rule as `/cbp-session-start` Step 5.7 — only commit files that are **not*
60
60
  - Collect `files[]` from those rounds → `task_files` set
61
61
  - If no active task exists, `task_files` is empty
62
62
  3. `infra_files = changed_files − task_files`
63
- 4. If `infra_files` is empty skip. Otherwise present once:
63
+ 4. Re-run `git status --porcelain` immediately before showing the commit-prompt (after Steps 1–1.3 have completed their MCP round-trips). Recompute `infra_files` from the fresh listing — eliminates the race where files appear in the index only after the network round-trips complete.
64
+ 5. If `infra_files` is empty → skip. Otherwise present once:
64
65
 
65
66
  ```
66
67
  Commit these non-task files before ending session?
@@ -69,7 +70,7 @@ Same rule as `/cbp-session-start` Step 5.7 — only commit files that are **not*
69
70
  Reply: yes | no | select
70
71
  ```
71
72
 
72
- 5. On `yes`: `git add` the listed files, then trigger `/cbp-git-commit`.
73
+ 6. On `yes`: `git add` the listed files, then trigger `/cbp-git-commit`.
73
74
  On `no`: skip. On `select`: ask which subset.
74
75
 
75
76
  Non-blocking — session end proceeds either way.
@@ -78,7 +79,7 @@ Non-blocking — session end proceeds either way.
78
79
 
79
80
  Keep this worktree's **home branch** rooted at the freshest production tip — no prompt, fully non-blocking. Home branches are the folder-named placeholder branches each worktree rests on (e.g. `codebyplan-web`); `feat/*` branches are deliberately skipped — they integrate via `/cbp-merge-main` with QA, never an auto fast-forward.
80
81
 
81
- Runs after Step 1.5 so any infra commits land first, and before Step 1.7 so the asset-update CLIs operate on the freshest tree.
82
+ Runs after Step 1.5 so any infra commits land first, and before Step 1.7 so the freshness gate's asset update operates on the freshest tree.
82
83
 
83
84
  Resolve the production branch: read `.codebyplan/git.json` and take `branch_config.production` (fall back to `main` if the file or field is absent). Call this `$PRODUCTION`. Then compare the current branch against the worktree's folder name:
84
85
 
@@ -99,83 +100,35 @@ CURRENT="$(git rev-parse --abbrev-ref HEAD)"
99
100
 
100
101
  Never rebase, reset, force-push, or stash. A non-fast-forwardable home branch is a signal to reconcile manually, not to overwrite.
101
102
 
102
- ### Step 1.7: Auto-Update CodeByPlan Assets
103
+ ### Step 1.7: Package Freshness Gate
103
104
 
104
- Run the latest published CLIs to pull any asset or config changes. Sub-block A runs first, then Sub-block B. A failure in A does **not** skip B each sub-block has its own retry-and-warn lane.
105
+ Check whether a newer `codebyplan` is published and safe to auto-install on this worktree's current branch, then run the local install + asset update. Runs after the Step 1.6 home-branch fast-forward (so any update lands on the freshest tree) and before Step 2. Fully non-blocking the guard and skip branches proceed silently; any failure in the install/update commands warns once and continues to Step 2. session-end never halts.
105
106
 
106
- **a) `codebyplan claude` (asset subcommand group)**
107
-
108
- Run the latest published CLI's asset-update verb to pull any skill / agent / hook changes:
109
-
110
- ```
111
- npx codebyplan@latest claude update
112
- ```
113
-
114
- Always run — do not skip, even when the current repo is the canonical-owner monorepo source.
115
-
116
- The command may pause for interactive prompts when:
117
-
118
- - a tracked file has been hand-edited locally (overwrite / skip / diff)
119
- - a new file is being shipped for the first time (opt-in / skip)
120
- - a file has been removed from the package (remove / keep)
121
-
122
- Respond to each prompt in the terminal as it appears. The commit prompt below runs only after all per-file prompts are resolved.
123
-
124
- Failure handling:
125
-
126
- - On non-zero exit: wait ~5 s, retry once.
127
- - If retry also fails: print a warning and continue — this step is non-blocking.
128
-
129
- After the command exits (success):
130
-
131
- 1. Run `git status --porcelain -- .claude/` to detect any file changes.
132
- 2. If non-empty, present once (same pattern as Step 1.5):
133
-
134
- ```
135
- .claude/ was updated. Commit these changes?
136
- [list of changed paths under .claude/]
137
-
138
- Reply: yes | no | select
139
- ```
140
-
141
- On `yes`: `git add` the listed paths only (not the whole `.claude/` directory), then trigger `/cbp-git-commit`.
142
- On `no`: skip. On `select`: ask which subset.
143
-
144
- 3. Print the final stdout line from the update command as a one-line session summary (e.g. `codebyplan claude update: 47 files tracked.`). Silent when the command produces no stdout.
145
-
146
- Non-blocking — session end proceeds regardless of outcome.
147
-
148
- **b) `codebyplan` (project CLI)**
149
-
150
- Fetch the latest published version of the project CLI. The `codebyplan` CLI has no `update` verb (subcommands are `setup`, `sync`, `eslint`); the freshness idiom is to invoke a fast read-only flag with `@latest` so npx fetches and caches the newest published version:
151
-
152
- ```
153
- npx codebyplan@latest --version
107
+ ```bash
108
+ VERSION_JSON=$(npx codebyplan version-status 2>/dev/null)
154
109
  ```
155
110
 
156
- Do **not** run `sync`, `setup`, or any other state-changing subcommand — this step updates the cached binary only.
157
-
158
- Failure handling:
159
-
160
- - On non-zero exit: wait ~5 s, retry once.
161
- - If retry also fails: print a warning and continue — this step is non-blocking.
162
-
163
- After the command exits (success):
111
+ Parse `$VERSION_JSON` as JSON and branch on the result:
164
112
 
165
- 1. Run `git status --porcelain -- .codebyplan/` to detect any file changes.
166
- 2. If non-empty, present once (same pattern as Step 1.5):
113
+ - **Probe failed / unparseable** — the command errored, produced no output, or the output cannot be parsed as JSON carrying the required keys (`newer`, `guarded`, `installCommand`). This includes an installed `codebyplan` too old to have the `version-status` subcommand (which prints `Unknown command`). → **FAIL-SAFE SKIP**: proceed silently to Step 2. A best-effort freshness probe must never disrupt session-end.
114
+ - **`guarded: true`** (protected/main branch OR the canonical `codebyplan` source monorepo — any `guardReason`) → skip silently, proceed to Step 2. Gate on the `guarded` boolean only; never branch on the specific `guardReason` string. This is the guard that prevents clobbering the canonical monorepo's ahead-of-published `.claude/` and blocks any update or commit on a protected branch.
115
+ - **`newer: false`** (already up to date) → skip silently, proceed to Step 2.
116
+ - **`newer: true` AND `guarded: false`** → run the update path:
117
+ 1. Run the JSON's `installCommand` (e.g. `pnpm add codebyplan@latest`) to LOCALLY install `codebyplan@latest` via the repo's package manager. Never a global `-g` install.
118
+ 2. Run `npx codebyplan claude update` to refresh the worktree's `.claude/` assets on the current branch. If `installCommand` (step 1) or this command exits non-zero, warn once and skip to Step 2 — this path is non-blocking.
119
+ 3. Detect changes across BOTH asset directories in one pass: `git status --porcelain -- .claude/ .codebyplan/`. If non-empty, present a single combined offer (same pattern as Step 1.5):
167
120
 
168
- ```
169
- .codebyplan/ was updated. Commit these changes?
170
- [list of changed paths under .codebyplan/]
121
+ ```
122
+ codebyplan updated. Commit these changes before ending the session?
123
+ [list of changed paths under .claude/ and .codebyplan/]
171
124
 
172
- Reply: yes | no | select
173
- ```
125
+ Reply: yes | no | select
126
+ ```
174
127
 
175
- On `yes`: `git add` the listed paths only, then trigger `/cbp-git-commit`.
176
- On `no`: skip. On `select`: ask which subset.
128
+ On `yes`: `git add` the listed paths only (not the whole directories), then trigger `/cbp-git-commit`. On `no`: skip. On `select`: ask which subset.
129
+ 4. Continue to Step 2 — session-end does NOT halt. The update lands in the current worktree on its current branch; main/protected branches and the canonical source repo are already excluded by the `guarded` check above.
177
130
 
178
- 3. Print the command's stdout (the version line) as a one-line session summary (e.g. `codebyplan 1.4.2`). Silent when the command produces no stdout.
131
+ **Home branches are intentionally not guarded.** A worktree's folder-named home branch (e.g. `codebyplan-web`) is neither protected/main nor the canonical source, so `version-status` returns `guarded:false` there and this gate runs the update exactly as on a feat branch — landing it on whichever branch the worktree currently rests on (the deliberate "update the branch they're on, except main/canonical" behavior).
179
132
 
180
133
  Non-blocking — session end proceeds regardless of outcome.
181
134
 
@@ -204,9 +157,9 @@ You can close this window.
204
157
  ## Integration
205
158
 
206
159
  - **Triggered by**: user invocation (prompted by `/cbp-todo` when no work remains)
207
- - **Reads**: `.codebyplan/repo.json`, `.codebyplan/git.json` (`branch_config.production` for the Step 1.6 home-branch fast-forward), MCP `get_session_logs` (resolve current log), MCP `get_current_task`, MCP `get_rounds`, MCP `get_next_action` (Step 1.3 handoff snapshot)
160
+ - **Reads**: `.codebyplan/repo.json`, `.codebyplan/git.json` (`branch_config.production` for the Step 1.6 home-branch fast-forward), MCP `get_session_logs` (resolve current log), MCP `get_current_task`, MCP `get_rounds`, MCP `get_next_action` (Step 1.3 handoff snapshot); `npx codebyplan version-status` (Step 1.7 package-freshness gate)
208
161
  - **Writes**: MCP `update_session_log` (with `ended_at` + `handoff` per TASK-2 alias surface; or `create_session_log` fallback), MCP `update_session_state` (deactivate)
209
162
  - **Spawns**: none
210
- - **Triggers**: none at the skill-contract level. Steps 1.5 and 1.7 may invoke `/cbp-git-commit` inline on user approval.
163
+ - **Triggers**: none at the skill-contract level. Step 1.5 may invoke `/cbp-git-commit` inline on user approval; Step 1.7 may invoke `/cbp-git-commit` on the `newer:true AND guarded:false` update path (committing changed `.claude/` and `.codebyplan/` paths).
211
164
  - **Paired with**: `/cbp-session-start`
212
- - **Pairs with**: `.claude/rules/session-resume.md` (handoff payload shape + write-path contract)
165
+ - **Pairs with**: `/cbp-session-start` Step 4.5 (handoff payload shape + freshness-gate contract; specified inline — no separate rule file)
@@ -12,32 +12,29 @@ Activate the session, open a fresh session log, and surface the previous log's p
12
12
 
13
13
  ## Instructions
14
14
 
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, Step 1.5 may surface a one-line infra-drift nudge, and Step 5.7 may surface an approval gate. (Step numbers are organizational labels; execution order is 0 → 1 → 1.4 → 1.5 → 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.
15
+ Run Steps 0 through 5.8 silently (no intermediate output) — except Step 0 aborts the session on MCP failure, Step 1.4 may surface a one-line fast-forward note or warning, Step 1.5 may surface a one-line infra-drift nudge, Step 1.6 may run an install-and-halt path, Step 4.5 may auto-resume a handoff and exit session-start entirely (no Step 6 output), and Step 5.7 may surface an approval gate. (Step numbers are organizational labels; execution order is 0 → 1 → 1.4 → 1.5 → 1.6 → 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
- ### Step 0: MCP Health Check
17
+ ### Step 0: MCP Health Gate
18
18
 
19
- Call MCP `health_check` tool.
19
+ Call MCP `health_check` tool. **The MCP connection is vital — this is a hard gate.**
20
20
 
21
- - **If succeeds**: Continue silently
22
- - **If fails**: Show warning:
21
+ - **If succeeds**: Continue silently to Step 1.
22
+ - **If fails**: Print the error below and **STOP the entire session-start**. Do NOT continue to Step 1 or any later step — no config load, no `update_session_state(activate)`, no `create_session_log`, no `/cbp-todo` trigger:
23
23
 
24
24
  ```
25
- MCP connection failed. Check:
25
+ MCP connection failed — session-start aborted. Check:
26
26
  1. Network connectivity
27
27
  2. API key validity (CODEBYPLAN_API_KEY env var)
28
28
  3. codebyplan.com availability
29
29
 
30
- Session continues but MCP-dependent features will be unavailable.
30
+ Fix the connection, then run /cbp-session-start again.
31
31
  ```
32
32
 
33
- Continue to Step 1 (non-blocking).
34
-
35
33
  ### Step 1: Load Config
36
34
 
37
35
  Read per-concern config files from the project root. Single load point for the session:
38
36
 
39
37
  - `repo_id` (UUID) — from `.codebyplan/repo.json`, required for all MCP operations
40
- - `server_port`, `server_type`, `auto_push_enabled` — from `.codebyplan/server.json`
41
38
  - `git_branch` — from `.codebyplan/git.json`
42
39
 
43
40
  Resolve `worktree_id` at runtime using the structured JSON form:
@@ -79,7 +76,7 @@ Never rebase, reset, force-push, or stash. A non-fast-forwardable home branch is
79
76
 
80
77
  ### Step 1.5: Infra Drift Check
81
78
 
82
- Surface — never block — when this worktree's CBP tooling has fallen behind. Runs after Step 1.4 and may add one line to the Step 6 output. Two mutually-exclusive concepts, keyed on repo type (`$PRODUCTION` is the branch resolved in Step 1.4):
79
+ Surface — never block — when this worktree's source-monorepo `.claude/` infra has fallen behind. Runs after Step 1.4 and may add one line to the Step 6 output (`$PRODUCTION` is the branch resolved in Step 1.4). Consumer-repo package-version freshness is handled by Step 1.6 (the freshness gate), not here:
83
80
 
84
81
  - **Monorepo (concept A)** — both `packages/codebyplan-package/templates/` and `scripts/infra-drift.mjs` exist. Step 1.4 skips the fetch on a feat branch, so refresh `origin/$PRODUCTION` best-effort first, then run the reporter:
85
82
 
@@ -90,26 +87,46 @@ Surface — never block — when this worktree's CBP tooling has fallen behind.
90
87
 
91
88
  The script self-guards (feat branch + behind > 0) and emits at most one `⚠ .claude/ infra is N behind — run /cbp-refresh-infra` line. Hold any output for Step 6.
92
89
 
93
- - **Consumer (concept B)** no `templates/`, but an install manifest exists (`.claude/.cbp.manifest.json`, falling back to `.cbp-claude.manifest.json` then `.codebyplan-claude.manifest.json`). Compare its `version` to the latest published `codebyplan`, offline-safe:
90
+ - **Neither** skip silently.
94
91
 
95
- ```bash
96
- LATEST="$(npm view codebyplan version 2>/dev/null)"
97
- ```
92
+ Fully non-blocking; every failure path falls through with no output.
98
93
 
99
- When `$LATEST` is non-empty and newer than the manifest `version`, hold one line for Step 6: `⚠ codebyplan {installed} → {LATEST} — run npx codebyplan@latest claude update`. Any failure (offline, npm absent) → silent.
94
+ ### Step 1.6: Package Freshness Gate
100
95
 
101
- - **Neither** skip silently.
96
+ Check whether a newer `codebyplan` is published and safe to auto-install on this worktree's current branch. Runs AFTER the infra-drift check (Step 1.5) and BEFORE session activation (Step 3).
97
+
98
+ ```bash
99
+ VERSION_JSON=$(npx codebyplan version-status 2>/dev/null)
100
+ ```
101
+
102
+ Parse `$VERSION_JSON` as JSON and branch on the result:
103
+
104
+ - **Probe failed / unparseable** — the command errored, produced no output, or the output cannot be parsed as JSON carrying the required keys (`newer`, `guarded`, `installCommand`). This includes an installed `codebyplan` too old to have the `version-status` subcommand (which prints `Unknown command`). → **FAIL-SAFE SKIP**: proceed silently to Step 3. Never disrupt a session over a best-effort freshness probe — the MCP gate (Step 0) is the only vital gate.
105
+ - **`guarded: true`** (protected/main branch OR the canonical `codebyplan` source monorepo — any `guardReason`) → skip silently, proceed to Step 3. Gate on the `guarded` boolean only; never branch on the specific `guardReason` string.
106
+ - **`newer: false`** (already up to date) → skip silently, proceed to Step 3.
107
+ - **`newer: true` AND `guarded: false`** → run the update path:
108
+ 1. Run the JSON's `installCommand` (e.g. `pnpm add codebyplan@latest`) to LOCALLY install `codebyplan@latest` via the repo's package manager. Never a global `-g` install.
109
+ 2. Run `npx codebyplan claude update` to refresh the worktree's `.claude/` assets on the current branch.
110
+ 3. Detect changes across BOTH asset directories in one pass: `git status --porcelain -- .claude/ .codebyplan/`. If non-empty, offer the same commit gate as Step 5.7:
111
+
112
+ ```
113
+ codebyplan updated. Commit the resulting .claude/ and .codebyplan/ changes before exiting?
114
+ [list of changed paths under .claude/ and .codebyplan/]
115
+
116
+ Reply: yes | no | select
117
+ ```
102
118
 
103
- Concept B never fires in the monorepo — the `templates/` guard routes the source repo to concept A only (its manifest `version` is intentionally stale). Fully non-blocking; every failure path falls through with no output.
119
+ On `yes`: `git add` the listed paths only, then trigger `/cbp-git-commit`. On `no`: skip. On `select`: ask which subset.
120
+ 4. **HALT** — do NOT proceed to Step 3. Print:
104
121
 
105
- ### Step 2: Check Dev Server
122
+ ```
123
+ ✓ codebyplan updated to {latest}. Start a FRESH Claude Code session
124
+ (run /clear or open a new window) so the updated .claude/ takes effect.
125
+ ```
106
126
 
107
- **Skip if `server_type` is `"none"`.**
127
+ On this update-and-halt path the session is NOT continued: `update_session_state(activate)` is NOT called, `create_session_log` is NOT called, and `/cbp-todo` is NOT triggered.
108
128
 
109
- 1. Get `server_port` from `.codebyplan/server.json`
110
- 2. Check if running: `lsof -ti:{PORT} 2>/dev/null`
111
- 3. If running, verify health: `curl -s -o /dev/null -w "%{http_code}" http://localhost:{PORT}/ --max-time 2`
112
- 4. If NOT running, note in output that user should start via desktop app
129
+ **Home branches are intentionally not guarded.** A worktree's folder-named home branch (e.g. `codebyplan-web`) is neither protected/main nor the canonical source, so `version-status` returns `guarded:false` there and this gate fires exactly as on a feat branch. That is deliberate — the update lands on whatever branch the worktree is currently resting on (the "update the branch they're on, except main/canonical" decision), not an accidental halt.
113
130
 
114
131
  ### Step 3: Update Session State
115
132
 
@@ -136,7 +153,7 @@ Hold the new log's ID in context so `/cbp-session-end` can update the same recor
136
153
 
137
154
  ### Step 4.5: Handoff Auto-Resume Probe
138
155
 
139
- Probe the most-recent closed session log for a structured handoff payload (per `.claude/rules/session-resume.md`) and auto-resume directly into the captured command when fresh. Additive — placed BEFORE the existing `/cbp-todo` auto-trigger; ALL failure paths fall through silently to Step 7.
156
+ Probe the most-recent closed session log for a structured handoff payload (the handoff freshness-gate contract is specified inline in this step) and auto-resume directly into the captured command when fresh. Additive — placed BEFORE the existing `/cbp-todo` auto-trigger; ALL failure paths fall through silently to Step 7.
140
157
 
141
158
  1. Reuse the row held from Step 4 (same `get_session_logs({ repo_id, worktree_id, limit: 1 })` call shape — no extra MCP round-trip).
142
159
  2. **Defensive gates** (any failure → silent fall-through to Step 7):
@@ -205,7 +222,6 @@ Ownership: [total_count] active CHK(s), [owned_count] owned by this worktree
205
222
  [Owned: CHK-NNN (title), … — only when owned_count > 0]
206
223
  [Cross-worktree: CHK-ZZZ (name), … — only when total_count > owned_count]
207
224
 
208
- [⚠ Dev server not running — start via desktop app — only if applicable]
209
225
  [⚠ Worktree unregistered — run `npx codebyplan setup` to register — only when WORKTREE_ID is null and no resolver distress was already shown]
210
226
  ```
211
227
 
@@ -223,9 +239,9 @@ Three-branch gate using `owned_count` and `total_count` from Step 5.8:
223
239
 
224
240
  - **Triggered by**: user invocation, `/clear` recovery
225
241
  - **Resolves**: `npx codebyplan resolve-worktree --json` (worktree id + distress signal; non-tuple-miss distress is non-blocking at session-start)
226
- - **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; `scripts/infra-drift.mjs` + a best-effort `git fetch` (Step 1.5 monorepo drift) or the install manifest + `npm view codebyplan version` (Step 1.5 consumer drift)
227
- - **Writes**: MCP `create_session_log` (new, possibly empty), MCP `update_session_state` (activate)
242
+ - **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` (Step 0 hard gate), 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; `scripts/infra-drift.mjs` + a best-effort `git fetch` (Step 1.5 monorepo drift); `npx codebyplan version-status` (Step 1.6 package-freshness gate). Reads at Step 3 and later (session-state, session logs, checkpoints, tasks/rounds) do NOT fire on a Step 0 MCP hard-fail or the Step 1.6 update-and-halt path
243
+ - **Writes**: MCP `create_session_log` (new, possibly empty), MCP `update_session_state` (activate) — both SKIPPED on a Step 0 MCP hard-fail and on the Step 1.6 update-and-halt path
228
244
  - **Spawns**: none
229
- - **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)
245
+ - **Triggers**: `/cbp-git-commit` (conditional, on user approval at Step 5.7 or the Step 1.6 update path), `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; NOT triggered on a Step 0 hard-fail or the Step 1.6 update-and-halt path)
230
246
  - **Paired with**: `/cbp-session-end`
231
- - **Pairs with**: `.claude/rules/session-resume.md` (handoff payload shape + freshness gate contract)
247
+ - **Pairs with**: `/cbp-session-end` Step 1.3 (handoff write-path; the freshness-gate contract is specified inline in Step 4.5 above)