codebyplan 1.13.15 → 1.13.16

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/README.md CHANGED
@@ -91,7 +91,7 @@ Print the CLI version.
91
91
 
92
92
  When you run Claude Code inside a [cmux](https://github.com/nicholasgasior/cmux) workspace, the `codebyplan` plugin automatically keeps the workspace metadata in sync with your git context:
93
93
 
94
- - **On session start** — the workspace title is set to the current git branch and the workspace description is set to the repo folder basename.
94
+ - **On session start** — the workspace title is set to the current git branch, the workspace description is set to the repo folder basename, and the workspace color is applied from `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). If no color is set, a one-line nudge is printed to session output.
95
95
  - **On `git checkout` / `git switch`** — the same sync runs automatically after the Bash tool call completes (the match is broad and a redundant sync on a file-restore checkout is harmless, since `codebyplan cmux-sync` is idempotent).
96
96
 
97
97
  This means your cmux workspace always reflects which branch and repo you're working in, without any manual intervention.
@@ -109,7 +109,7 @@ Both hooks delegate all logic to `codebyplan cmux-sync` — no cmux or git logic
109
109
 
110
110
  ### Binary resolution order
111
111
 
112
- The `cmux` binary is resolved in this order (by `codebyplan cmux-sync`):
112
+ The `cmux` binary is resolved in this order (by `codebyplan cmux-sync` and `codebyplan cmux-status`, both delegating to `resolveCmuxBin()` in `lib/cmux.ts`):
113
113
 
114
114
  1. `$CMUX_BUNDLED_CLI_PATH` — path cmux injects into its Claude hook environment
115
115
  2. `$CMUX_CLAUDE_HOOK_CMUX_BIN` — alternative env var the hook environment may set
@@ -119,6 +119,81 @@ The `cmux` binary is resolved in this order (by `codebyplan cmux-sync`):
119
119
 
120
120
  Both hooks check for `$CMUX_WORKSPACE_ID` before doing anything. If you are not running inside a cmux workspace, the hooks exit immediately with no output and no side effects. Repos that do not use cmux are completely unaffected.
121
121
 
122
+ ### Status surface
123
+
124
+ `codebyplan cmux-status` pushes CodeByPlan development state into the cmux workspace sidebar. The lifecycle skills (`cbp-task-start`, `cbp-task-complete`, `cbp-round-execute`) call it automatically — no manual invocation is needed.
125
+
126
+ **Flags:**
127
+
128
+ | Flag | Description |
129
+ | --------------------- | --------------------------------------------------------------- |
130
+ | `--checkpoint <text>` | Set the `cbp-checkpoint` status key |
131
+ | `--task <text>` | Set the `cbp-task` status key |
132
+ | `--qa <text>` | Set the `cbp-qa` status key |
133
+ | `--progress <val>` | Set the sidebar progress bar (0.0–1.0 float, or `N/D` fraction) |
134
+ | `--clear` | Clear all four keys from the sidebar |
135
+
136
+ **Examples:**
137
+
138
+ ```bash
139
+ # Pushed by cbp-task-start when you start a task:
140
+ npx codebyplan cmux-status --checkpoint "CHK-176: cmux status" --task "TASK-2: implement cmux-status"
141
+
142
+ # Pushed by cbp-task-complete when a task finishes:
143
+ npx codebyplan cmux-status --task "TASK-2: implement cmux-status done" --progress 2/5
144
+
145
+ # Pushed by cbp-round-execute after each round:
146
+ npx codebyplan cmux-status --qa "R1 completed"
147
+
148
+ # Clear all status keys:
149
+ npx codebyplan cmux-status --clear
150
+ ```
151
+
152
+ **`auto_status` toggle.** The command is gated by the `auto_status` field in `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). When `auto_status` is `false`, every call is a no-op. The default is `true` — status pushing is enabled as long as the file exists or is absent (absent defaults to enabled).
153
+
154
+ **No-op outside cmux.** Like `codebyplan cmux-sync`, this command checks for `$CMUX_WORKSPACE_ID` before doing anything. Outside a cmux workspace it exits immediately with no output, so it is safe to call unconditionally from scripts and hooks.
155
+
156
+ ### Auto dev server (`codebyplan cmux-serve`)
157
+
158
+ `codebyplan cmux-serve` auto-starts the dev server for any app whose source files are touched by the current round, and opens a cmux browser pane pointing at the allocated port. `cbp-round-execute` calls it automatically at round-execution start — no manual invocation is needed.
159
+
160
+ **How it works:**
161
+
162
+ The command receives the round's changed file list via `--files <comma-separated paths>`. It reads `.codebyplan/server.json` `port_allocations[]` and resolves which apps' source directories intersect the file list. For each matching app it:
163
+
164
+ 1. Probes the allocated port (500ms `node:net` connection attempt — no `curl`, no `timeout`).
165
+ 2. If the port is **not** listening: creates a `cmux new-split down` terminal pane and sends the dev command via `cmux send`.
166
+ 3. Opens a browser pane via `cmux new-pane --type browser --url http://localhost:<port>`.
167
+
168
+ If the port is already listening (another worktree, or a previously started server) it skips straight to step 3 — only the browser pane is opened. This mitigates the multi-worktree port collision by never starting a second server on an already-occupied port.
169
+
170
+ **App-source-dir heuristic mapping** (from `port_allocations[].label` when `command` / `working_dir` are null):
171
+
172
+ | Label | App dir |
173
+ | ------------------------------ | -------------------------- |
174
+ | `Web Dev` | `apps/web` |
175
+ | `Web Dev (codebyplan-desktop)` | `apps/desktop` |
176
+ | `Backend Dev` | `apps/backend` |
177
+ | `MCP Dev` | `apps/mcp` |
178
+ | `Docs Ingest` | `apps/docs-ingest` |
179
+ | `E2E Tests` | skipped (not a dev server) |
180
+
181
+ When `allocation.command` and `allocation.working_dir` are both set (Tier 1), those values are used directly — the label heuristic is skipped. When no mapping exists for a label, one log line is emitted and the allocation is skipped; the rest continue.
182
+
183
+ **`auto_dev_server` toggle.** Gated by the `auto_dev_server` field in `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). When `auto_dev_server` is `false`, every call is a no-op. The default is `true` — the feature is enabled as long as the file is absent or the key is not explicitly set to `false`.
184
+
185
+ **No-op outside cmux.** Like `codebyplan cmux-status`, this command checks for `$CMUX_WORKSPACE_ID` before doing anything. Outside a cmux workspace it exits immediately with no output and no side effects.
186
+
187
+ **Examples:**
188
+
189
+ ```bash
190
+ # Called automatically by cbp-round-execute with the round's files:
191
+ npx codebyplan cmux-serve --files "apps/web/src/app/page.tsx,apps/web/src/components/Button.tsx"
192
+
193
+ # Target a specific app dir instead of a file list:
194
+ npx codebyplan cmux-serve --app apps/backend
195
+ ```
196
+
122
197
  ---
123
198
 
124
199
  ## MCP Server
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.15";
17
+ VERSION = "1.13.16";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -1987,6 +1987,11 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1987
1987
  JSON.stringify({}, null, 2) + "\n",
1988
1988
  "utf-8"
1989
1989
  );
1990
+ await writeFile6(
1991
+ join9(codebyplanDir, "cmux.json"),
1992
+ JSON.stringify({}, null, 2) + "\n",
1993
+ "utf-8"
1994
+ );
1990
1995
  const statuslinePath = join9(codebyplanDir, "statusline.json");
1991
1996
  let statuslineExists = false;
1992
1997
  try {
@@ -2004,7 +2009,7 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
2004
2009
  await writeLocalConfig(projectPath, { device_id: deviceId });
2005
2010
  console.log(` Created ${codebyplanDir}/`);
2006
2011
  console.log(
2007
- ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, eslint.json, statusline.json`
2012
+ ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, eslint.json, cmux.json, statusline.json`
2008
2013
  );
2009
2014
  console.log(` device.local.json (gitignored)`);
2010
2015
  const gitignoreAction = await ensureManagedGitignoreBlock(projectPath);
@@ -2081,8 +2086,8 @@ async function runSetup() {
2081
2086
  const deviceId = await getOrCreateDeviceId(projectPath);
2082
2087
  let branch = "main";
2083
2088
  try {
2084
- const { execSync: execSync9 } = await import("node:child_process");
2085
- branch = execSync9("git symbolic-ref --short HEAD", {
2089
+ const { execSync: execSync11 } = await import("node:child_process");
2090
+ branch = execSync11("git symbolic-ref --short HEAD", {
2086
2091
  cwd: projectPath,
2087
2092
  encoding: "utf-8"
2088
2093
  }).trim();
@@ -3720,9 +3725,9 @@ async function eslintInit(repoId, projectPath) {
3720
3725
  Install ${missingPkgs.length} missing packages? [Y/n] `
3721
3726
  );
3722
3727
  if (confirmed) {
3723
- const { execSync: execSync9 } = await import("node:child_process");
3728
+ const { execSync: execSync11 } = await import("node:child_process");
3724
3729
  try {
3725
- execSync9(installCmd, { cwd: projectPath, stdio: "inherit" });
3730
+ execSync11(installCmd, { cwd: projectPath, stdio: "inherit" });
3726
3731
  console.log(" Packages installed.\n");
3727
3732
  } catch (err) {
3728
3733
  console.error(
@@ -6623,6 +6628,42 @@ var init_upload_e2e_images = __esm({
6623
6628
  }
6624
6629
  });
6625
6630
 
6631
+ // src/lib/cmux.ts
6632
+ import { readFileSync as readFileSync6 } from "node:fs";
6633
+ import { join as join22 } from "node:path";
6634
+ function insideCmux() {
6635
+ return !!process.env.CMUX_WORKSPACE_ID;
6636
+ }
6637
+ function resolveCmuxBin() {
6638
+ return process.env.CMUX_BUNDLED_CLI_PATH || process.env.CMUX_CLAUDE_HOOK_CMUX_BIN || "cmux";
6639
+ }
6640
+ function readCmuxConfig(projectRoot) {
6641
+ let raw = {};
6642
+ try {
6643
+ const text = readFileSync6(
6644
+ join22(projectRoot, ".codebyplan", "cmux.json"),
6645
+ "utf-8"
6646
+ );
6647
+ raw = JSON.parse(text);
6648
+ } catch {
6649
+ raw = {};
6650
+ }
6651
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
6652
+ raw = {};
6653
+ }
6654
+ const config = raw;
6655
+ return {
6656
+ ...config,
6657
+ auto_status: config.auto_status ?? true,
6658
+ auto_dev_server: config.auto_dev_server ?? true
6659
+ };
6660
+ }
6661
+ var init_cmux = __esm({
6662
+ "src/lib/cmux.ts"() {
6663
+ "use strict";
6664
+ }
6665
+ });
6666
+
6626
6667
  // src/cli/cmux-sync.ts
6627
6668
  var cmux_sync_exports = {};
6628
6669
  __export(cmux_sync_exports, {
@@ -6632,10 +6673,10 @@ import { execSync as execSync8, execFileSync as execFileSync2 } from "node:child
6632
6673
  import { basename as basename2 } from "node:path";
6633
6674
  async function runCmuxSync() {
6634
6675
  try {
6635
- if (!process.env.CMUX_WORKSPACE_ID) {
6676
+ if (!insideCmux()) {
6636
6677
  process.exit(0);
6637
6678
  }
6638
- const bin = process.env.CMUX_BUNDLED_CLI_PATH || process.env.CMUX_CLAUDE_HOOK_CMUX_BIN || "cmux";
6679
+ const bin = resolveCmuxBin();
6639
6680
  let branch = "";
6640
6681
  try {
6641
6682
  branch = execSync8("git rev-parse --abbrev-ref HEAD", {
@@ -6644,8 +6685,9 @@ async function runCmuxSync() {
6644
6685
  } catch {
6645
6686
  }
6646
6687
  let folder = "";
6688
+ let toplevel = "";
6647
6689
  try {
6648
- const toplevel = execSync8("git rev-parse --show-toplevel", {
6690
+ toplevel = execSync8("git rev-parse --show-toplevel", {
6649
6691
  encoding: "utf8"
6650
6692
  }).trim();
6651
6693
  folder = basename2(toplevel);
@@ -6675,6 +6717,26 @@ async function runCmuxSync() {
6675
6717
  } catch {
6676
6718
  }
6677
6719
  }
6720
+ try {
6721
+ const cmuxCfg = readCmuxConfig(toplevel || process.cwd());
6722
+ if (typeof cmuxCfg.workspace_color === "string" && cmuxCfg.workspace_color !== "") {
6723
+ try {
6724
+ execFileSync2(bin, [
6725
+ "workspace-action",
6726
+ "--action",
6727
+ "set-color",
6728
+ "--color",
6729
+ cmuxCfg.workspace_color
6730
+ ]);
6731
+ } catch {
6732
+ }
6733
+ } else {
6734
+ process.stdout.write(
6735
+ "cmux: no workspace color set \u2014 run /cbp-setup-cmux\n"
6736
+ );
6737
+ }
6738
+ } catch {
6739
+ }
6678
6740
  process.exit(0);
6679
6741
  } catch (err) {
6680
6742
  if (err instanceof ProcessExitSignal) throw err;
@@ -6685,23 +6747,346 @@ var init_cmux_sync = __esm({
6685
6747
  "src/cli/cmux-sync.ts"() {
6686
6748
  "use strict";
6687
6749
  init_process_exit_signal();
6750
+ init_cmux();
6751
+ }
6752
+ });
6753
+
6754
+ // src/cli/cmux-status.ts
6755
+ var cmux_status_exports = {};
6756
+ __export(cmux_status_exports, {
6757
+ normalizeProgress: () => normalizeProgress,
6758
+ runCmuxStatus: () => runCmuxStatus
6759
+ });
6760
+ import { execSync as execSync9, execFileSync as execFileSync3 } from "node:child_process";
6761
+ function normalizeProgress(raw) {
6762
+ if (raw.includes("/")) {
6763
+ const [numStr, denStr] = raw.split("/", 2);
6764
+ const num = parseInt(numStr ?? "", 10);
6765
+ const den = parseInt(denStr ?? "", 10);
6766
+ if (!Number.isFinite(num) || !Number.isFinite(den) || den === 0) return "0";
6767
+ const ratio = num / den;
6768
+ const clamped2 = Math.max(0, Math.min(1, ratio));
6769
+ return String(clamped2);
6770
+ }
6771
+ const f = parseFloat(raw);
6772
+ if (!Number.isFinite(f)) return null;
6773
+ const clamped = Math.max(0, Math.min(1, f));
6774
+ return String(clamped);
6775
+ }
6776
+ async function runCmuxStatus(args) {
6777
+ try {
6778
+ if (!insideCmux()) {
6779
+ process.exit(0);
6780
+ }
6781
+ let toplevel = "";
6782
+ try {
6783
+ toplevel = execSync9("git rev-parse --show-toplevel", {
6784
+ encoding: "utf8"
6785
+ }).trim();
6786
+ } catch {
6787
+ toplevel = process.cwd();
6788
+ }
6789
+ const cfg = readCmuxConfig(toplevel);
6790
+ if (cfg.auto_status === false) {
6791
+ process.exit(0);
6792
+ }
6793
+ let checkpoint;
6794
+ let task;
6795
+ let qa;
6796
+ let progress;
6797
+ let clear = false;
6798
+ for (let i = 0; i < args.length; i++) {
6799
+ const flag = args[i];
6800
+ if (flag === "--checkpoint" && i + 1 < args.length) {
6801
+ checkpoint = args[++i];
6802
+ } else if (flag === "--task" && i + 1 < args.length) {
6803
+ task = args[++i];
6804
+ } else if (flag === "--qa" && i + 1 < args.length) {
6805
+ qa = args[++i];
6806
+ } else if (flag === "--progress" && i + 1 < args.length) {
6807
+ progress = args[++i];
6808
+ } else if (flag === "--clear") {
6809
+ clear = true;
6810
+ }
6811
+ }
6812
+ const bin = resolveCmuxBin();
6813
+ if (clear) {
6814
+ try {
6815
+ execFileSync3(bin, ["clear-status", "cbp-checkpoint"]);
6816
+ } catch {
6817
+ }
6818
+ try {
6819
+ execFileSync3(bin, ["clear-status", "cbp-task"]);
6820
+ } catch {
6821
+ }
6822
+ try {
6823
+ execFileSync3(bin, ["clear-status", "cbp-qa"]);
6824
+ } catch {
6825
+ }
6826
+ try {
6827
+ execFileSync3(bin, ["clear-progress"]);
6828
+ } catch {
6829
+ }
6830
+ } else {
6831
+ if (checkpoint !== void 0) {
6832
+ try {
6833
+ execFileSync3(bin, ["set-status", "cbp-checkpoint", checkpoint]);
6834
+ } catch {
6835
+ }
6836
+ }
6837
+ if (task !== void 0) {
6838
+ try {
6839
+ execFileSync3(bin, ["set-status", "cbp-task", task]);
6840
+ } catch {
6841
+ }
6842
+ }
6843
+ if (qa !== void 0) {
6844
+ try {
6845
+ execFileSync3(bin, ["set-status", "cbp-qa", qa]);
6846
+ } catch {
6847
+ }
6848
+ }
6849
+ if (progress !== void 0) {
6850
+ const decimalStr = normalizeProgress(progress);
6851
+ if (decimalStr !== null) {
6852
+ try {
6853
+ execFileSync3(bin, ["set-progress", decimalStr]);
6854
+ } catch {
6855
+ }
6856
+ }
6857
+ }
6858
+ }
6859
+ process.exit(0);
6860
+ } catch (err) {
6861
+ if (err instanceof ProcessExitSignal) throw err;
6862
+ process.exit(0);
6863
+ }
6864
+ }
6865
+ var init_cmux_status = __esm({
6866
+ "src/cli/cmux-status.ts"() {
6867
+ "use strict";
6868
+ init_process_exit_signal();
6869
+ init_cmux();
6870
+ }
6871
+ });
6872
+
6873
+ // src/cli/cmux-serve.ts
6874
+ var cmux_serve_exports = {};
6875
+ __export(cmux_serve_exports, {
6876
+ probePort: () => probePort,
6877
+ runCmuxServe: () => runCmuxServe
6878
+ });
6879
+ import { execSync as execSync10, execFileSync as execFileSync4 } from "node:child_process";
6880
+ import { readFileSync as readFileSync7 } from "node:fs";
6881
+ import * as net from "node:net";
6882
+ import { join as join23 } from "node:path";
6883
+ function resolveAppDir(allocation, toplevel) {
6884
+ if (allocation.command !== null && allocation.working_dir !== null) {
6885
+ const wd = allocation.working_dir;
6886
+ const dir = wd.startsWith(toplevel + "/") ? wd.slice(toplevel.length + 1) : wd;
6887
+ return { appDir: dir, devCommand: allocation.command };
6888
+ }
6889
+ const label = allocation.label ?? "";
6890
+ if (label === "E2E Tests") return { skip: "skip-e2e" };
6891
+ if (label.includes("Web Dev") && !label.toLowerCase().includes("desktop")) {
6892
+ return { appDir: "apps/web", devCommand: null };
6893
+ }
6894
+ if (label.toLowerCase().includes("desktop")) {
6895
+ return { appDir: "apps/desktop", devCommand: null };
6896
+ }
6897
+ const appDir = LABEL_APP_MAP[label];
6898
+ if (appDir !== void 0) {
6899
+ return { appDir, devCommand: null };
6900
+ }
6901
+ return { skip: "no-match" };
6902
+ }
6903
+ function probePort(port) {
6904
+ return new Promise((resolve8) => {
6905
+ const socket = new net.Socket();
6906
+ let settled = false;
6907
+ const settle = (result) => {
6908
+ if (!settled) {
6909
+ settled = true;
6910
+ socket.destroy();
6911
+ resolve8(result);
6912
+ }
6913
+ };
6914
+ socket.setTimeout(500);
6915
+ socket.on("connect", () => settle(true));
6916
+ socket.on("error", () => settle(false));
6917
+ socket.on("timeout", () => settle(false));
6918
+ socket.connect({ port, host: "127.0.0.1" });
6919
+ });
6920
+ }
6921
+ async function runCmuxServe(args) {
6922
+ try {
6923
+ if (!insideCmux()) {
6924
+ process.exit(0);
6925
+ }
6926
+ let toplevel = "";
6927
+ try {
6928
+ toplevel = execSync10("git rev-parse --show-toplevel", {
6929
+ encoding: "utf8"
6930
+ }).trim();
6931
+ } catch {
6932
+ toplevel = process.cwd();
6933
+ }
6934
+ const cfg = readCmuxConfig(toplevel);
6935
+ if (cfg.auto_dev_server === false) {
6936
+ process.exit(0);
6937
+ }
6938
+ let filesArg;
6939
+ let appArg;
6940
+ for (let i = 0; i < args.length; i++) {
6941
+ const flag = args[i];
6942
+ if (flag === "--files" && i + 1 < args.length) {
6943
+ filesArg = args[++i];
6944
+ } else if (flag === "--app" && i + 1 < args.length) {
6945
+ appArg = args[++i];
6946
+ }
6947
+ }
6948
+ const changedFiles = filesArg !== void 0 ? filesArg.split(",").map((f) => f.trim()).filter(Boolean) : [];
6949
+ let serverConfig = null;
6950
+ try {
6951
+ const raw = readFileSync7(
6952
+ join23(toplevel, ".codebyplan", "server.json"),
6953
+ "utf-8"
6954
+ );
6955
+ serverConfig = JSON.parse(raw);
6956
+ } catch {
6957
+ process.exit(0);
6958
+ }
6959
+ const allocations = serverConfig?.port_allocations ?? [];
6960
+ if (allocations.length === 0) {
6961
+ process.exit(0);
6962
+ }
6963
+ const bin = resolveCmuxBin();
6964
+ for (const allocation of allocations) {
6965
+ try {
6966
+ const resolved = resolveAppDir(allocation, toplevel);
6967
+ if ("skip" in resolved) {
6968
+ if (resolved.skip === "no-match") {
6969
+ process.stdout.write(
6970
+ `cmux-serve: no app mapping for allocation "${allocation.label ?? ""}" \u2014 skipped
6971
+ `
6972
+ );
6973
+ }
6974
+ continue;
6975
+ }
6976
+ const { appDir, devCommand } = resolved;
6977
+ const appDirWithSlash = appDir + "/";
6978
+ const intersects = appArg !== void 0 && (appArg === appDir || appArg.startsWith(appDirWithSlash)) || changedFiles.some(
6979
+ (f) => f === appDir || f.startsWith(appDirWithSlash)
6980
+ );
6981
+ if (!intersects) {
6982
+ continue;
6983
+ }
6984
+ const port = allocation.port;
6985
+ const listening = await probePort(port);
6986
+ if (!listening) {
6987
+ let shellCommand = null;
6988
+ if (devCommand !== null) {
6989
+ shellCommand = `cd "${join23(toplevel, appDir)}" && ${devCommand}`;
6990
+ } else {
6991
+ let hasDev = false;
6992
+ try {
6993
+ const pkgRaw = readFileSync7(
6994
+ join23(toplevel, appDir, "package.json"),
6995
+ "utf-8"
6996
+ );
6997
+ const pkg = JSON.parse(pkgRaw);
6998
+ hasDev = typeof pkg.scripts?.dev === "string";
6999
+ } catch {
7000
+ }
7001
+ if (!hasDev) {
7002
+ process.stdout.write(
7003
+ `cmux-serve: no "dev" script in ${appDir}/package.json \u2014 skipped
7004
+ `
7005
+ );
7006
+ continue;
7007
+ }
7008
+ shellCommand = `cd "${join23(toplevel, appDir)}" && pnpm run dev`;
7009
+ }
7010
+ let splitSurfaceRef = null;
7011
+ try {
7012
+ const splitOut = execFileSync4(
7013
+ bin,
7014
+ ["new-split", "down", "--json"],
7015
+ {
7016
+ encoding: "utf8"
7017
+ }
7018
+ );
7019
+ const parsed = JSON.parse(splitOut);
7020
+ if (typeof parsed.surface_ref === "string" && parsed.surface_ref) {
7021
+ splitSurfaceRef = parsed.surface_ref;
7022
+ }
7023
+ } catch {
7024
+ }
7025
+ if (splitSurfaceRef !== null) {
7026
+ try {
7027
+ execFileSync4(bin, [
7028
+ "send",
7029
+ "--surface",
7030
+ splitSurfaceRef,
7031
+ `${shellCommand}
7032
+ `
7033
+ ]);
7034
+ } catch {
7035
+ }
7036
+ } else {
7037
+ process.stdout.write(
7038
+ `cmux-serve: could not resolve new split surface for ${appDir} \u2014 dev server not auto-started (open it manually)
7039
+ `
7040
+ );
7041
+ }
7042
+ }
7043
+ try {
7044
+ execFileSync4(bin, [
7045
+ "new-pane",
7046
+ "--type",
7047
+ "browser",
7048
+ "--url",
7049
+ `http://localhost:${port}`
7050
+ ]);
7051
+ } catch {
7052
+ }
7053
+ } catch {
7054
+ }
7055
+ }
7056
+ process.exit(0);
7057
+ } catch (err) {
7058
+ if (err instanceof ProcessExitSignal) throw err;
7059
+ process.exit(0);
7060
+ }
7061
+ }
7062
+ var LABEL_APP_MAP;
7063
+ var init_cmux_serve = __esm({
7064
+ "src/cli/cmux-serve.ts"() {
7065
+ "use strict";
7066
+ init_process_exit_signal();
7067
+ init_cmux();
7068
+ LABEL_APP_MAP = {
7069
+ "Backend Dev": "apps/backend",
7070
+ "MCP Dev": "apps/mcp",
7071
+ "Docs Ingest": "apps/docs-ingest"
7072
+ };
6688
7073
  }
6689
7074
  });
6690
7075
 
6691
7076
  // src/lib/migrate-local-config.ts
6692
7077
  import { mkdir as mkdir6, readFile as readFile16, unlink as unlink2, writeFile as writeFile12 } from "node:fs/promises";
6693
- import { join as join22 } from "node:path";
7078
+ import { join as join24 } from "node:path";
6694
7079
  function legacySharedPath(projectPath) {
6695
- return join22(projectPath, ".codebyplan.json");
7080
+ return join24(projectPath, ".codebyplan.json");
6696
7081
  }
6697
7082
  function legacyLocalPath(projectPath) {
6698
- return join22(projectPath, ".codebyplan.local.json");
7083
+ return join24(projectPath, ".codebyplan.local.json");
6699
7084
  }
6700
7085
  function newDirPath(projectPath) {
6701
- return join22(projectPath, ".codebyplan");
7086
+ return join24(projectPath, ".codebyplan");
6702
7087
  }
6703
7088
  function sentinelPath(projectPath) {
6704
- return join22(projectPath, ".codebyplan", "repo.json");
7089
+ return join24(projectPath, ".codebyplan", "repo.json");
6705
7090
  }
6706
7091
  async function statSafe(p) {
6707
7092
  const { stat: stat2 } = await import("node:fs/promises");
@@ -6795,7 +7180,7 @@ async function runLocalMigration(projectPath) {
6795
7180
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
6796
7181
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
6797
7182
  await writeFile12(
6798
- join22(projectPath, ".codebyplan", "repo.json"),
7183
+ join24(projectPath, ".codebyplan", "repo.json"),
6799
7184
  JSON.stringify(repoJson, null, 2) + "\n",
6800
7185
  "utf-8"
6801
7186
  );
@@ -6808,7 +7193,7 @@ async function runLocalMigration(projectPath) {
6808
7193
  if ("port_allocations" in cfg)
6809
7194
  serverJson.port_allocations = cfg.port_allocations;
6810
7195
  await writeFile12(
6811
- join22(projectPath, ".codebyplan", "server.json"),
7196
+ join24(projectPath, ".codebyplan", "server.json"),
6812
7197
  JSON.stringify(serverJson, null, 2) + "\n",
6813
7198
  "utf-8"
6814
7199
  );
@@ -6817,7 +7202,7 @@ async function runLocalMigration(projectPath) {
6817
7202
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
6818
7203
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
6819
7204
  await writeFile12(
6820
- join22(projectPath, ".codebyplan", "git.json"),
7205
+ join24(projectPath, ".codebyplan", "git.json"),
6821
7206
  JSON.stringify(gitJson, null, 2) + "\n",
6822
7207
  "utf-8"
6823
7208
  );
@@ -6825,35 +7210,35 @@ async function runLocalMigration(projectPath) {
6825
7210
  const shipmentJson = {};
6826
7211
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
6827
7212
  await writeFile12(
6828
- join22(projectPath, ".codebyplan", "shipment.json"),
7213
+ join24(projectPath, ".codebyplan", "shipment.json"),
6829
7214
  JSON.stringify(shipmentJson, null, 2) + "\n",
6830
7215
  "utf-8"
6831
7216
  );
6832
7217
  filesChanged.push(".codebyplan/shipment.json");
6833
7218
  const vendorJson = {};
6834
7219
  await writeFile12(
6835
- join22(projectPath, ".codebyplan", "vendor.json"),
7220
+ join24(projectPath, ".codebyplan", "vendor.json"),
6836
7221
  JSON.stringify(vendorJson, null, 2) + "\n",
6837
7222
  "utf-8"
6838
7223
  );
6839
7224
  filesChanged.push(".codebyplan/vendor.json");
6840
7225
  const e2eJson = {};
6841
7226
  await writeFile12(
6842
- join22(projectPath, ".codebyplan", "e2e.json"),
7227
+ join24(projectPath, ".codebyplan", "e2e.json"),
6843
7228
  JSON.stringify(e2eJson, null, 2) + "\n",
6844
7229
  "utf-8"
6845
7230
  );
6846
7231
  filesChanged.push(".codebyplan/e2e.json");
6847
7232
  const eslintJson = {};
6848
7233
  await writeFile12(
6849
- join22(projectPath, ".codebyplan", "eslint.json"),
7234
+ join24(projectPath, ".codebyplan", "eslint.json"),
6850
7235
  JSON.stringify(eslintJson, null, 2) + "\n",
6851
7236
  "utf-8"
6852
7237
  );
6853
7238
  filesChanged.push(".codebyplan/eslint.json");
6854
7239
  if (!deviceWrittenByHelper) {
6855
7240
  await writeFile12(
6856
- join22(projectPath, ".codebyplan", "device.local.json"),
7241
+ join24(projectPath, ".codebyplan", "device.local.json"),
6857
7242
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
6858
7243
  "utf-8"
6859
7244
  );
@@ -6865,7 +7250,7 @@ async function runLocalMigration(projectPath) {
6865
7250
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
6866
7251
  );
6867
7252
  }
6868
- const gitignorePath = join22(projectPath, ".gitignore");
7253
+ const gitignorePath = join24(projectPath, ".gitignore");
6869
7254
  try {
6870
7255
  const gitignoreContent = await readFile16(gitignorePath, "utf-8");
6871
7256
  const legacyLine = ".codebyplan.local.json";
@@ -6927,7 +7312,7 @@ __export(config_exports, {
6927
7312
  runConfig: () => runConfig
6928
7313
  });
6929
7314
  import { mkdir as mkdir7, readFile as readFile17, writeFile as writeFile13 } from "node:fs/promises";
6930
- import { join as join23 } from "node:path";
7315
+ import { join as join25 } from "node:path";
6931
7316
  async function runConfig() {
6932
7317
  const flags = parseFlags(3);
6933
7318
  const dryRun = hasFlag("dry-run", 3);
@@ -6960,14 +7345,14 @@ async function runConfig() {
6960
7345
  console.log("\n Config complete.\n");
6961
7346
  }
6962
7347
  async function syncConfigToFile(repoId, projectPath, dryRun) {
6963
- const codebyplanDir = join23(projectPath, ".codebyplan");
7348
+ const codebyplanDir = join25(projectPath, ".codebyplan");
6964
7349
  let resolvedWorktreeId;
6965
7350
  try {
6966
7351
  const deviceId = await getOrCreateDeviceId(projectPath);
6967
7352
  let branch = "main";
6968
7353
  try {
6969
- const { execSync: execSync9 } = await import("node:child_process");
6970
- branch = execSync9("git symbolic-ref --short HEAD", {
7354
+ const { execSync: execSync11 } = await import("node:child_process");
7355
+ branch = execSync11("git symbolic-ref --short HEAD", {
6971
7356
  cwd: projectPath,
6972
7357
  encoding: "utf-8"
6973
7358
  }).trim();
@@ -7087,6 +7472,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
7087
7472
  const vendorPayload = {};
7088
7473
  const e2ePayload = {};
7089
7474
  const eslintPayload = {};
7475
+ const cmuxPayload = {};
7090
7476
  if (dryRun) {
7091
7477
  console.log(" Config would be updated (dry-run).");
7092
7478
  return;
@@ -7099,11 +7485,12 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
7099
7485
  { name: "shipment.json", payload: shipmentPayload },
7100
7486
  { name: "vendor.json", payload: vendorPayload },
7101
7487
  { name: "e2e.json", payload: e2ePayload, createOnly: true },
7102
- { name: "eslint.json", payload: eslintPayload, createOnly: true }
7488
+ { name: "eslint.json", payload: eslintPayload, createOnly: true },
7489
+ { name: "cmux.json", payload: cmuxPayload, createOnly: true }
7103
7490
  ];
7104
7491
  let anyUpdated = false;
7105
7492
  for (const { name, payload, createOnly } of files) {
7106
- const filePath = join23(codebyplanDir, name);
7493
+ const filePath = join25(codebyplanDir, name);
7107
7494
  const newJson = JSON.stringify(payload, null, 2) + "\n";
7108
7495
  let currentJson = "";
7109
7496
  try {
@@ -7123,7 +7510,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
7123
7510
  async function readRepoConfig(projectPath) {
7124
7511
  try {
7125
7512
  const raw = await readFile17(
7126
- join23(projectPath, ".codebyplan", "repo.json"),
7513
+ join25(projectPath, ".codebyplan", "repo.json"),
7127
7514
  "utf-8"
7128
7515
  );
7129
7516
  return JSON.parse(raw);
@@ -7134,7 +7521,7 @@ async function readRepoConfig(projectPath) {
7134
7521
  async function readServerConfig(projectPath) {
7135
7522
  try {
7136
7523
  const raw = await readFile17(
7137
- join23(projectPath, ".codebyplan", "server.json"),
7524
+ join25(projectPath, ".codebyplan", "server.json"),
7138
7525
  "utf-8"
7139
7526
  );
7140
7527
  return JSON.parse(raw);
@@ -7145,7 +7532,7 @@ async function readServerConfig(projectPath) {
7145
7532
  async function readGitConfig(projectPath) {
7146
7533
  try {
7147
7534
  const raw = await readFile17(
7148
- join23(projectPath, ".codebyplan", "git.json"),
7535
+ join25(projectPath, ".codebyplan", "git.json"),
7149
7536
  "utf-8"
7150
7537
  );
7151
7538
  return JSON.parse(raw);
@@ -7156,7 +7543,7 @@ async function readGitConfig(projectPath) {
7156
7543
  async function readShipmentConfig(projectPath) {
7157
7544
  try {
7158
7545
  const raw = await readFile17(
7159
- join23(projectPath, ".codebyplan", "shipment.json"),
7546
+ join25(projectPath, ".codebyplan", "shipment.json"),
7160
7547
  "utf-8"
7161
7548
  );
7162
7549
  return JSON.parse(raw);
@@ -7167,7 +7554,7 @@ async function readShipmentConfig(projectPath) {
7167
7554
  async function readVendorConfig(projectPath) {
7168
7555
  try {
7169
7556
  const raw = await readFile17(
7170
- join23(projectPath, ".codebyplan", "vendor.json"),
7557
+ join25(projectPath, ".codebyplan", "vendor.json"),
7171
7558
  "utf-8"
7172
7559
  );
7173
7560
  return JSON.parse(raw);
@@ -7178,7 +7565,7 @@ async function readVendorConfig(projectPath) {
7178
7565
  async function readE2eConfig2(projectPath) {
7179
7566
  try {
7180
7567
  const raw = await readFile17(
7181
- join23(projectPath, ".codebyplan", "e2e.json"),
7568
+ join25(projectPath, ".codebyplan", "e2e.json"),
7182
7569
  "utf-8"
7183
7570
  );
7184
7571
  return JSON.parse(raw);
@@ -7512,10 +7899,10 @@ async function runTechStack() {
7512
7899
  );
7513
7900
  }
7514
7901
  try {
7515
- const { execSync: execSync9 } = await import("node:child_process");
7902
+ const { execSync: execSync11 } = await import("node:child_process");
7516
7903
  let branch = "main";
7517
7904
  try {
7518
- branch = execSync9("git symbolic-ref --short HEAD", {
7905
+ branch = execSync11("git symbolic-ref --short HEAD", {
7519
7906
  cwd: projectPath,
7520
7907
  encoding: "utf-8"
7521
7908
  }).trim();
@@ -8314,13 +8701,13 @@ var init_uninstall = __esm({
8314
8701
 
8315
8702
  // src/index.ts
8316
8703
  init_version();
8317
- import { readFileSync as readFileSync8 } from "node:fs";
8704
+ import { readFileSync as readFileSync10 } from "node:fs";
8318
8705
  import { resolve as resolve7 } from "node:path";
8319
8706
  void (async () => {
8320
8707
  if (!process.env.CODEBYPLAN_API_KEY) {
8321
8708
  try {
8322
8709
  const envPath = resolve7(process.cwd(), ".env.local");
8323
- const content = readFileSync8(envPath, "utf-8");
8710
+ const content = readFileSync10(envPath, "utf-8");
8324
8711
  for (const line of content.split("\n")) {
8325
8712
  const trimmed = line.trim();
8326
8713
  if (!trimmed || trimmed.startsWith("#")) continue;
@@ -8463,6 +8850,18 @@ void (async () => {
8463
8850
  await runCmuxSync2();
8464
8851
  process.exit(0);
8465
8852
  }
8853
+ if (arg === "cmux-status") {
8854
+ const { runCmuxStatus: runCmuxStatus2 } = await Promise.resolve().then(() => (init_cmux_status(), cmux_status_exports));
8855
+ const rest = process.argv.slice(3);
8856
+ await runCmuxStatus2(rest);
8857
+ process.exit(0);
8858
+ }
8859
+ if (arg === "cmux-serve") {
8860
+ const { runCmuxServe: runCmuxServe2 } = await Promise.resolve().then(() => (init_cmux_serve(), cmux_serve_exports));
8861
+ const rest = process.argv.slice(3);
8862
+ await runCmuxServe2(rest);
8863
+ process.exit(0);
8864
+ }
8466
8865
  if (arg === "config") {
8467
8866
  const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
8468
8867
  await runConfig2();
@@ -8567,6 +8966,8 @@ void (async () => {
8567
8966
  codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
8568
8967
  codebyplan version-status Report installed vs latest version + update guard (JSON)
8569
8968
  codebyplan cmux-sync Sync cmux workspace title/description to current git branch and repo folder
8969
+ codebyplan cmux-status Push checkpoint/task/QA + progress to the cmux workspace sidebar
8970
+ codebyplan cmux-serve Auto-start dev server + browser pane for the round's app files (cmux)
8570
8971
  codebyplan help Show this help message
8571
8972
  codebyplan --version Print version
8572
8973
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.15",
3
+ "version": "1.13.16",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -226,7 +226,7 @@ After a `complete_round` MCP call succeeds, reconciles the round's `files_change
226
226
 
227
227
  ### `cbp-cmux-workspace-sync.sh` — SessionStart, matcher `*`
228
228
 
229
- On every session start, syncs the active [cmux](https://github.com/nicholasgasior/cmux) workspace title to the current git branch and the workspace description to the repo folder basename (the directory that contains `.git/`).
229
+ On every session start, syncs the active [cmux](https://github.com/nicholasgasior/cmux) workspace title to the current git branch, the workspace description to the repo folder basename (the directory that contains `.git/`), and applies the workspace color from `.codebyplan/cmux.json` via `cmux workspace-action --action set-color`. All three actions are delegated to `codebyplan cmux-sync`. If no `workspace_color` is configured, a one-line nudge is printed to stdout prompting the user to run `/cbp-setup-cmux`.
230
230
 
231
231
  **Blocks vs warns**: never blocks — exit 0 on every path. A SessionStart hook must never prevent a session from opening.
232
232
 
@@ -252,6 +252,28 @@ After any Bash tool call that contains a `git checkout` or `git switch` invocati
252
252
 
253
253
  ---
254
254
 
255
+ ### Auto dev server (`codebyplan cmux-serve`)
256
+
257
+ At the start of each round, `cbp-round-execute` (Step 2a) calls `codebyplan cmux-serve --files "<round files>"` to auto-start the dev server for any app whose source files are touched. The subcommand probes each allocated port via `node:net`, starts a `cmux new-split` terminal pane + sends the dev command for any non-listening app, then opens a browser pane. If the port is already listening (another worktree) it only opens the browser pane. No hook registration is needed — the skill invokes the subcommand directly. Gated by `auto_dev_server` in `.codebyplan/cmux.json`; no-op outside cmux.
258
+
259
+ ---
260
+
261
+ ### Status surface (`codebyplan cmux-status`)
262
+
263
+ The lifecycle skills push CodeByPlan development state into the cmux workspace sidebar via `codebyplan cmux-status`. No hook registration is needed — the skills invoke the subcommand directly:
264
+
265
+ | Skill | What is pushed |
266
+ | --- | --- |
267
+ | `cbp-task-start` (Step 4.5) | `--checkpoint "CHK-NNN: title" --task "TASK-N: title"` |
268
+ | `cbp-task-complete` (Step 7.3) | `--task "TASK-N: title done" --progress completed/total` |
269
+ | `cbp-round-execute` (Step 3d) | `--qa "R{n} {status}"` where status ∈ completed / blocked / re-triggering |
270
+
271
+ **`auto_status` toggle.** Gated by the `auto_status` field in `.codebyplan/cmux.json` (configured via `/cbp-setup-cmux`). When `auto_status` is `false`, every call is a no-op. Default is `true` (enabled).
272
+
273
+ **No-op outside cmux.** `codebyplan cmux-status` checks for `$CMUX_WORKSPACE_ID` before doing anything. Outside a cmux workspace it exits immediately — safe to call unconditionally from skills and hooks.
274
+
275
+ ---
276
+
255
277
  ## Supporting (not registered)
256
278
 
257
279
  ### `test-hooks.sh` — invoked by `auto-test-hooks.sh`
@@ -124,6 +124,7 @@
124
124
  "Skill(cbp-round-update)",
125
125
  "Skill(cbp-session-end)",
126
126
  "Skill(cbp-session-start)",
127
+ "Skill(cbp-setup-cmux)",
127
128
  "Skill(cbp-setup-e2e)",
128
129
  "Skill(cbp-setup-eslint)",
129
130
  "Skill(cbp-ship-configure)",
@@ -135,6 +136,11 @@
135
136
  "Skill(cbp-supabase-branch-check)",
136
137
  "Skill(cbp-supabase-migrate)",
137
138
  "Skill(cbp-supabase-setup)",
139
+ "Skill(cbp-standalone-task-check)",
140
+ "Skill(cbp-standalone-task-complete)",
141
+ "Skill(cbp-standalone-task-create)",
142
+ "Skill(cbp-standalone-task-start)",
143
+ "Skill(cbp-standalone-task-testing)",
138
144
  "Skill(cbp-task-check)",
139
145
  "Skill(cbp-task-complete)",
140
146
  "Skill(cbp-task-create)",
@@ -196,6 +202,10 @@
196
202
  "Bash(npx codebyplan resolve-worktree:*)",
197
203
  "Bash(codebyplan cmux-sync:*)",
198
204
  "Bash(npx codebyplan cmux-sync:*)",
205
+ "Bash(codebyplan cmux-status:*)",
206
+ "Bash(npx codebyplan cmux-status:*)",
207
+ "Bash(codebyplan cmux-serve:*)",
208
+ "Bash(npx codebyplan cmux-serve:*)",
199
209
  "Bash(codebyplan version-status:*)",
200
210
  "Bash(npx codebyplan version-status:*)",
201
211
  "Bash(codebyplan statusline:*)",
@@ -57,6 +57,16 @@ Read the plan from round context (`context.planner_output`). If no plan: `No app
57
57
 
58
58
  Read effective testing profile: `round.context.testing_profile_override` if set (user override for this round only), else `task.context.testing_profile` (set by planner Phase 4.8), else default `'web'`. Pass the effective profile to all per-wave `cbp-testing-qa-agent` spawns.
59
59
 
60
+ ### Step 2a: Auto-Dev-Server (cmux)
61
+
62
+ Fire the dev-server hook at round-execution start. Self-no-ops outside cmux or when `auto_dev_server` is disabled in `.codebyplan/cmux.json`.
63
+
64
+ ```bash
65
+ npx codebyplan cmux-serve --files "<comma-separated approved_plan.files_to_modify[].path>"
66
+ ```
67
+
68
+ The subcommand reads `.codebyplan/server.json` `port_allocations[]`, resolves which apps' source dirs intersect the round's files, probes each allocated port, and starts a cmux terminal split + browser pane for any app not already serving. Idempotent — if the port is already listening it only opens the browser pane (mitigating the multi-worktree port collision).
69
+
60
70
  ### Step 3: Route Execution Path
61
71
 
62
72
  Inspect `approved_plan.files_to_modify[]` and `approved_plan.round_type`. Four execution paths exist; pick the one that matches BEFORE Step 3a/3b.
@@ -143,6 +153,14 @@ If the approved plan includes database schema changes, RLS policies, or type gen
143
153
  - `status: 'blocked'` → present blocker to user via AskUserQuestion, resolve, re-spawn executor with remaining work
144
154
  - Deliverables incomplete → re-spawn executor with remaining deliverables (max 3 re-triggers). After 3 re-triggers, save partial output and proceed.
145
155
 
156
+ ### Step 3d: Push cmux QA Status
157
+
158
+ Push the round's QA outcome to the cmux workspace sidebar. Self-no-ops outside cmux or when `auto_status` is disabled. Status is one of: `completed`, `blocked`, or `re-triggering`.
159
+
160
+ ```bash
161
+ npx codebyplan cmux-status --qa "R{round_number} {status}"
162
+ ```
163
+
146
164
  ### Step 4: Dev-Server Probe (rounds 2+, web/desktop profile)
147
165
 
148
166
  When `round_number >= 2` AND `testing_profile` is `'web'` or `'desktop'` AND `files_changed` contains any UI file, probe the dev server BEFORE cbp-testing-qa-agent spawns (saves a full agent spawn when the server is down).
@@ -0,0 +1,170 @@
1
+ ---
2
+ scope: org-shared
3
+ name: cbp-setup-cmux
4
+ description: Configure .codebyplan/cmux.json — choose workspace color and toggle auto_status / auto_dev_server. Interactive, idempotent.
5
+ argument-hint: "[--force]"
6
+ model: sonnet
7
+ effort: xhigh
8
+ allowed-tools: Read, Write, Edit, Bash(cat *), Bash(jq *), Bash(test *), Bash(mv *), Bash(git check-ignore *), Bash(cmux *), AskUserQuestion
9
+ ---
10
+
11
+ # cmux Setup
12
+
13
+ Configure `.codebyplan/cmux.json` so the cmux workspace integration knows which color to
14
+ apply on session start and whether auto_status / auto_dev_server are enabled.
15
+
16
+ Invoke at any time. Existing values are preserved unless `--force` is passed.
17
+ Pass `--force` to re-ask all questions including already-configured fields.
18
+
19
+ ## Arguments
20
+
21
+ Inspect `$ARGUMENTS` for `--force`. If present, set `force_mode = true`.
22
+ Absent: use idempotent mode — preserve existing configured values, skip re-asking
23
+ already-set fields.
24
+
25
+ ## Step 1 — Parse --force and read existing config
26
+
27
+ ```bash
28
+ cat .codebyplan/cmux.json 2>/dev/null || echo '{}'
29
+ ```
30
+
31
+ Capture the result as `EXISTING_JSON`. Extract existing values:
32
+
33
+ ```bash
34
+ EXISTING_COLOR=$(echo "$EXISTING_JSON" | jq -r '.workspace_color // empty')
35
+ EXISTING_AUTO_STATUS=$(echo "$EXISTING_JSON" | jq -r '.auto_status // empty')
36
+ EXISTING_AUTO_DEV=$(echo "$EXISTING_JSON" | jq -r '.auto_dev_server // empty')
37
+ ```
38
+
39
+ ## Step 2 — Ask for workspace color
40
+
41
+ **Idempotency gate**: skip this question if `force_mode = false` AND `EXISTING_COLOR`
42
+ is a non-empty string. Print the preserved value and continue to Step 3.
43
+
44
+ Otherwise, AskUserQuestion:
45
+
46
+ ```
47
+ Workspace color for this cmux workspace
48
+
49
+ Choose a named color or enter a custom hex value (#RRGGBB).
50
+
51
+ Named colors:
52
+ A) Red B) Crimson C) Orange D) Amber
53
+ E) Olive F) Green G) Teal H) Aqua
54
+ I) Blue J) Navy K) Indigo L) Purple
55
+ M) Magenta N) Rose O) Brown P) Charcoal
56
+ Q) Custom hex — enter a value in the format #RRGGBB
57
+
58
+ Enter a letter (A-Q) or type a custom hex value directly:
59
+ ```
60
+
61
+ Validate the input:
62
+ - If a letter A-P: map to the corresponding color name (e.g. "A" → "Red").
63
+ - If "Q": prompt the user to type a custom hex value, then validate the typed value
64
+ against `^#[0-9A-Fa-f]{6}$`. If invalid, re-prompt once with an error. Never pass the
65
+ bare letter "Q" to set-color.
66
+ - If the raw input itself already looks like a hex value: validate it against
67
+ `^#[0-9A-Fa-f]{6}$` and accept on match.
68
+ - Empty input with no existing color: use no color (leave `workspace_color` as `null`).
69
+
70
+ Set `NEW_COLOR` to the chosen color name / hex, or `null` if skipped.
71
+
72
+ ## Step 3 — Ask for auto_status and auto_dev_server toggles
73
+
74
+ **Idempotency gate**: skip questions for fields that are already set (non-empty in
75
+ existing config) when `force_mode = false`.
76
+
77
+ For each unset field (or all fields when `force_mode = true`), AskUserQuestion:
78
+
79
+ ```
80
+ cmux integration toggles
81
+
82
+ auto_status: automatically run `cmux status` in this workspace on session start?
83
+ (default: on)
84
+ A) On
85
+ B) Off
86
+
87
+ auto_dev_server: automatically start the dev server via cmux on session start?
88
+ (default: on)
89
+ A) On
90
+ B) Off
91
+ ```
92
+
93
+ Ask both in a single question when both are unset. Set `NEW_AUTO_STATUS` and
94
+ `NEW_AUTO_DEV` to `true` or `false` based on the answers.
95
+
96
+ ## Step 4 — Write .codebyplan/cmux.json
97
+
98
+ Build the updated payload using jq deep-merge so sibling fields added by future
99
+ schema versions are preserved. Assemble variables first.
100
+
101
+ Backfill any field that the Step 2/3 idempotency gates skipped, so each `jq
102
+ --argjson` always receives valid JSON — an empty shell variable makes `jq` exit 1,
103
+ which would leave `NEW_PAYLOAD` empty and silently no-op the write. Fall back to the
104
+ existing value, then to the default (`true` for the toggles):
105
+
106
+ ```bash
107
+ NEW_COLOR="${NEW_COLOR:-$EXISTING_COLOR}"
108
+ NEW_AUTO_STATUS="${NEW_AUTO_STATUS:-${EXISTING_AUTO_STATUS:-true}}"
109
+ NEW_AUTO_DEV="${NEW_AUTO_DEV:-${EXISTING_AUTO_DEV:-true}}"
110
+ ```
111
+
112
+ ```bash
113
+ NEW_COLOR_JSON=$(echo "$NEW_COLOR" | jq -R 'if . == "null" or . == "" then null else . end')
114
+ NEW_PAYLOAD=$(jq -n \
115
+ --argjson color "$NEW_COLOR_JSON" \
116
+ --argjson auto_status "$NEW_AUTO_STATUS" \
117
+ --argjson auto_dev_server "$NEW_AUTO_DEV" \
118
+ '{workspace_color: $color, auto_status: $auto_status, auto_dev_server: $auto_dev_server}')
119
+ ```
120
+
121
+ Atomic write via jq tmp+mv deep-merge (`. * $new` deep-merges, preserving any
122
+ sibling keys not in the new payload):
123
+
124
+ ```bash
125
+ jq --argjson new "$NEW_PAYLOAD" '. * $new' \
126
+ .codebyplan/cmux.json > .codebyplan/cmux.json.tmp \
127
+ && mv .codebyplan/cmux.json.tmp .codebyplan/cmux.json
128
+ ```
129
+
130
+ ## Step 5 — Apply color immediately
131
+
132
+ If `NEW_COLOR` is a non-empty, non-null string AND cmux is available in the current
133
+ environment (`$CMUX_WORKSPACE_ID` is set), apply the color immediately so the change
134
+ is visible without needing to restart the session:
135
+
136
+ ```bash
137
+ if [ -n "$CMUX_WORKSPACE_ID" ] && [ -n "$NEW_COLOR" ] && [ "$NEW_COLOR" != "null" ]; then
138
+ CMUX_BIN="${CMUX_BUNDLED_CLI_PATH:-${CMUX_CLAUDE_HOOK_CMUX_BIN:-cmux}}"
139
+ "$CMUX_BIN" workspace-action --action set-color --color "$NEW_COLOR" 2>/dev/null || true
140
+ fi
141
+ ```
142
+
143
+ The guard (`2>/dev/null || true`) ensures a missing or non-zero cmux never blocks
144
+ the skill — applying the color is best-effort.
145
+
146
+ ## Step 6 — Verify and report
147
+
148
+ Re-read `.codebyplan/cmux.json` and emit a summary:
149
+
150
+ ```
151
+ cmux Setup — Complete
152
+
153
+ workspace_color : <value or "(none)">
154
+ auto_status : <true|false>
155
+ auto_dev_server : <true|false>
156
+
157
+ Config written to .codebyplan/cmux.json
158
+
159
+ Next: on the next SessionStart, codebyplan cmux-sync will apply the color
160
+ automatically. Run `/cbp-setup-cmux --force` at any time to reconfigure.
161
+ ```
162
+
163
+ ## Key Rules
164
+
165
+ - Atomic write (tmp + mv) — never leaves cmux.json in a partial state
166
+ - Deep-merge with `. * $new` — sibling keys from future schema versions are preserved
167
+ - Color apply is best-effort — a missing cmux binary never causes an error
168
+ - `auto_status` and `auto_dev_server` default to `true` when absent (documented in
169
+ `packages/codebyplan-package/src/lib/types.ts` CmuxConfig)
170
+ - cmux.json is COMMITTED (not gitignored) — workspace color is shared across team members
@@ -138,6 +138,20 @@ Skip the push only when nothing was committed in Step 5 AND `/cbp-merge-main` re
138
138
 
139
139
  Call `complete_task(task_id)`. The server resolves the caller's worktree identity from the JWT/ctx and enforces the mutate-lock (CHK-140 TASK-3 — `caller_worktree_id` input field removed). The server auto-clears `assigned_user_id` + `assigned_worktree_id` on the task; if this was the last sibling task, it also clears the parent checkpoint's assignment. (Per CHK-104 hard-lock.)
140
140
 
141
+ ### Step 7.3: Push cmux Status (task done + progress)
142
+
143
+ Push completion status and checkpoint progress to the cmux workspace sidebar. Self-no-ops outside cmux or when `auto_status` is disabled.
144
+
145
+ Compute progress from the `get_tasks` data already loaded by the routing step: `completed = count of tasks with status 'completed'`, `total = total task count for the checkpoint`. For standalone tasks (no checkpoint), omit `--progress`.
146
+
147
+ ```bash
148
+ # Checkpoint-bound task:
149
+ npx codebyplan cmux-status --task "TASK-{N}: {task-title} done" --progress {completed}/{total}
150
+
151
+ # Standalone task:
152
+ npx codebyplan cmux-status --task "TASK-{N}: {task-title} done"
153
+ ```
154
+
141
155
  ### Step 8: Run Cleanup + Migration (inline)
142
156
 
143
157
  Apply the `cleanup` skill inline to remove orphan references to deleted/modified files. Then apply `migration` to propagate renames/moves to consumers. Both run without sub-agent spawns. Skip cleanup if no deletions/modifications; skip migration if cleanup handled everything.
@@ -228,6 +228,14 @@ Display context summary:
228
228
  - **Previous rounds**: [count] completed
229
229
  ```
230
230
 
231
+ ### Step 4.5: Push cmux Status
232
+
233
+ Push the active checkpoint and task context to the cmux workspace sidebar. Self-no-ops outside cmux or when `auto_status` is disabled in `.codebyplan/cmux.json`.
234
+
235
+ ```bash
236
+ npx codebyplan cmux-status --checkpoint "CHK-{NNN}: {checkpoint-title}" --task "TASK-{N}: {task-title}"
237
+ ```
238
+
231
239
  ### Step 5: Set Task Status
232
240
 
233
241
  Use MCP `update_task(task_id, status: "in_progress")`.