codebyplan 1.13.3 → 1.13.5

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
@@ -87,6 +87,40 @@ Show help message.
87
87
 
88
88
  Print the CLI version.
89
89
 
90
+ ## cmux integration
91
+
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
+
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.
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
+
97
+ This means your cmux workspace always reflects which branch and repo you're working in, without any manual intervention.
98
+
99
+ ### How it works
100
+
101
+ Two hooks handle the sync:
102
+
103
+ | Hook | Event | Trigger |
104
+ | ---------------------------- | ------------------ | ------------------------------------------ |
105
+ | `cbp-cmux-workspace-sync.sh` | `SessionStart` | Every new Claude Code session |
106
+ | `cbp-cmux-branch-watch.sh` | `PostToolUse Bash` | Any `git checkout` or `git switch` command |
107
+
108
+ Both hooks delegate all logic to `codebyplan cmux-sync` — no cmux or git logic lives in the shell scripts.
109
+
110
+ ### Binary resolution order
111
+
112
+ The `cmux` binary is resolved in this order (by `codebyplan cmux-sync`):
113
+
114
+ 1. `$CMUX_BUNDLED_CLI_PATH` — path cmux injects into its Claude hook environment
115
+ 2. `$CMUX_CLAUDE_HOOK_CMUX_BIN` — alternative env var the hook environment may set
116
+ 3. `cmux` on `$PATH` — fallback to the system-installed binary
117
+
118
+ ### No-op outside cmux
119
+
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
+
122
+ ---
123
+
90
124
  ## MCP Server
91
125
 
92
126
  Claude Code connects to CodeByPlan via a remote MCP server. The `setup` command configures this automatically:
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.3";
17
+ VERSION = "1.13.5";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
@@ -1449,8 +1449,8 @@ async function runSetup() {
1449
1449
  const deviceId = await getOrCreateDeviceId(projectPath);
1450
1450
  let branch = "main";
1451
1451
  try {
1452
- const { execSync: execSync6 } = await import("node:child_process");
1453
- branch = execSync6("git symbolic-ref --short HEAD", {
1452
+ const { execSync: execSync7 } = await import("node:child_process");
1453
+ branch = execSync7("git symbolic-ref --short HEAD", {
1454
1454
  cwd: projectPath,
1455
1455
  encoding: "utf-8"
1456
1456
  }).trim();
@@ -3069,9 +3069,9 @@ async function eslintInit(repoId, projectPath) {
3069
3069
  Install ${missingPkgs.length} missing packages? [Y/n] `
3070
3070
  );
3071
3071
  if (confirmed) {
3072
- const { execSync: execSync6 } = await import("node:child_process");
3072
+ const { execSync: execSync7 } = await import("node:child_process");
3073
3073
  try {
3074
- execSync6(installCmd, { cwd: projectPath, stdio: "inherit" });
3074
+ execSync7(installCmd, { cwd: projectPath, stdio: "inherit" });
3075
3075
  console.log(" Packages installed.\n");
3076
3076
  } catch (err) {
3077
3077
  console.error(
@@ -5586,10 +5586,25 @@ var init_scaffold_publish_workflow2 = __esm({
5586
5586
  }
5587
5587
  });
5588
5588
 
5589
+ // src/cli/process-exit-signal.ts
5590
+ var ProcessExitSignal;
5591
+ var init_process_exit_signal = __esm({
5592
+ "src/cli/process-exit-signal.ts"() {
5593
+ "use strict";
5594
+ ProcessExitSignal = class extends Error {
5595
+ code;
5596
+ constructor(code) {
5597
+ super(`process.exit(${code})`);
5598
+ this.name = "ProcessExitSignal";
5599
+ this.code = code;
5600
+ }
5601
+ };
5602
+ }
5603
+ });
5604
+
5589
5605
  // src/cli/resolve-worktree.ts
5590
5606
  var resolve_worktree_exports = {};
5591
5607
  __export(resolve_worktree_exports, {
5592
- ProcessExitSignal: () => ProcessExitSignal,
5593
5608
  runResolveWorktree: () => runResolveWorktree
5594
5609
  });
5595
5610
  import { execSync as execSync5 } from "node:child_process";
@@ -5713,21 +5728,78 @@ function emitAndExit(worktreeId, errorContext, jsonMode) {
5713
5728
  }
5714
5729
  process.exit(0);
5715
5730
  }
5716
- var ProcessExitSignal;
5717
5731
  var init_resolve_worktree2 = __esm({
5718
5732
  "src/cli/resolve-worktree.ts"() {
5719
5733
  "use strict";
5720
5734
  init_flags();
5721
5735
  init_local_config();
5722
5736
  init_resolve_worktree();
5723
- ProcessExitSignal = class extends Error {
5724
- code;
5725
- constructor(code) {
5726
- super(`process.exit(${code})`);
5727
- this.name = "ProcessExitSignal";
5728
- this.code = code;
5737
+ init_process_exit_signal();
5738
+ }
5739
+ });
5740
+
5741
+ // src/cli/cmux-sync.ts
5742
+ var cmux_sync_exports = {};
5743
+ __export(cmux_sync_exports, {
5744
+ runCmuxSync: () => runCmuxSync
5745
+ });
5746
+ import { execSync as execSync6, execFileSync } from "node:child_process";
5747
+ import { basename } from "node:path";
5748
+ async function runCmuxSync() {
5749
+ try {
5750
+ if (!process.env.CMUX_WORKSPACE_ID) {
5751
+ process.exit(0);
5752
+ }
5753
+ const bin = process.env.CMUX_BUNDLED_CLI_PATH || process.env.CMUX_CLAUDE_HOOK_CMUX_BIN || "cmux";
5754
+ let branch = "";
5755
+ try {
5756
+ branch = execSync6("git rev-parse --abbrev-ref HEAD", {
5757
+ encoding: "utf8"
5758
+ }).trim();
5759
+ } catch {
5760
+ }
5761
+ let folder = "";
5762
+ try {
5763
+ const toplevel = execSync6("git rev-parse --show-toplevel", {
5764
+ encoding: "utf8"
5765
+ }).trim();
5766
+ folder = basename(toplevel);
5767
+ } catch {
5768
+ }
5769
+ if (branch) {
5770
+ try {
5771
+ execFileSync(bin, [
5772
+ "workspace-action",
5773
+ "--action",
5774
+ "rename",
5775
+ "--title",
5776
+ branch
5777
+ ]);
5778
+ } catch {
5729
5779
  }
5730
- };
5780
+ }
5781
+ if (folder) {
5782
+ try {
5783
+ execFileSync(bin, [
5784
+ "workspace-action",
5785
+ "--action",
5786
+ "set-description",
5787
+ "--description",
5788
+ folder
5789
+ ]);
5790
+ } catch {
5791
+ }
5792
+ }
5793
+ process.exit(0);
5794
+ } catch (err) {
5795
+ if (err instanceof ProcessExitSignal) throw err;
5796
+ process.exit(0);
5797
+ }
5798
+ }
5799
+ var init_cmux_sync = __esm({
5800
+ "src/cli/cmux-sync.ts"() {
5801
+ "use strict";
5802
+ init_process_exit_signal();
5731
5803
  }
5732
5804
  });
5733
5805
 
@@ -6009,8 +6081,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
6009
6081
  const deviceId = await getOrCreateDeviceId(projectPath);
6010
6082
  let branch = "main";
6011
6083
  try {
6012
- const { execSync: execSync6 } = await import("node:child_process");
6013
- branch = execSync6("git symbolic-ref --short HEAD", {
6084
+ const { execSync: execSync7 } = await import("node:child_process");
6085
+ branch = execSync7("git symbolic-ref --short HEAD", {
6014
6086
  cwd: projectPath,
6015
6087
  encoding: "utf-8"
6016
6088
  }).trim();
@@ -6555,10 +6627,10 @@ async function runTechStack() {
6555
6627
  );
6556
6628
  }
6557
6629
  try {
6558
- const { execSync: execSync6 } = await import("node:child_process");
6630
+ const { execSync: execSync7 } = await import("node:child_process");
6559
6631
  let branch = "main";
6560
6632
  try {
6561
- branch = execSync6("git symbolic-ref --short HEAD", {
6633
+ branch = execSync7("git symbolic-ref --short HEAD", {
6562
6634
  cwd: projectPath,
6563
6635
  encoding: "utf-8"
6564
6636
  }).trim();
@@ -7467,6 +7539,11 @@ void (async () => {
7467
7539
  await runResolveWorktree2();
7468
7540
  process.exit(0);
7469
7541
  }
7542
+ if (arg === "cmux-sync") {
7543
+ const { runCmuxSync: runCmuxSync2 } = await Promise.resolve().then(() => (init_cmux_sync(), cmux_sync_exports));
7544
+ await runCmuxSync2();
7545
+ process.exit(0);
7546
+ }
7470
7547
  if (arg === "config") {
7471
7548
  const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
7472
7549
  await runConfig2();
@@ -7565,6 +7642,7 @@ void (async () => {
7565
7642
  codebyplan claude Claude asset management (install/update/uninstall)
7566
7643
  codebyplan statusline Show or set the statusline renderer (bash/node/python)
7567
7644
  codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
7645
+ codebyplan cmux-sync Sync cmux workspace title/description to current git branch and repo folder
7568
7646
  codebyplan help Show this help message
7569
7647
  codebyplan --version Print version
7570
7648
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.13.3",
3
+ "version": "1.13.5",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `codebyplan` npm package ships a small, portable set of Claude Code hooks. They run in your project, use only generic primitives (`git rev-parse`, `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_PLUGIN_ROOT}`), and degrade gracefully (exit 0) when their preconditions aren't met.
4
4
 
5
- Hook registration lives in [`hooks/hooks.json`](./hooks.json) — PreToolUse, PostToolUse, and Notification events are wired. (`SessionStart`, `SessionEnd`, `Stop`, and `SubagentStop` are also schema-permitted but unused here.)
5
+ Hook registration lives in [`hooks/hooks.json`](./hooks.json) — SessionStart, PreToolUse, and PostToolUse events are wired. (`Notification`, `SessionEnd`, `Stop`, and `SubagentStop` are also schema-permitted but unused here.)
6
6
 
7
7
  **`cbp-statusline.sh` is auto-wired via `settings.project.base.json`.** The `statusLine` block is shipped inside `templates/settings.project.base.json` and merged into the consumer's `.claude/settings.json` automatically by `codebyplan claude install` (and on every `codebyplan claude update`). No manual copy-paste is required.
8
8
 
@@ -176,22 +176,6 @@ Walks up from the edited file to the nearest `package.json` and uses that packag
176
176
 
177
177
  ---
178
178
 
179
- ### `notify.sh` — Notification, matcher `*`
180
-
181
- Sends a desktop notification when Claude Code emits a notification event (waiting for input, task complete, etc.). Title is `[<project-name>] Claude Code`; body is the notification message; clicking the notification focuses VS Code at the project directory (macOS only).
182
-
183
- **Cross-platform graceful skip**: exits 0 silently when `terminal-notifier` is not on `$PATH`. Linux, Windows, and macOS hosts without Homebrew see a no-op — no errors, no warnings.
184
-
185
- **VS Code click action**: only attached when running on macOS (`$OSTYPE` matches `darwin*`) AND the `code` CLI is on `$PATH`. On other hosts the notification still fires but is non-clickable.
186
-
187
- **Install hint** (macOS): `brew install terminal-notifier`. On other platforms the hook is a no-op — substitute your own notification mechanism via a settings.json override if desired.
188
-
189
- **Blocks vs warns**: never blocks — exit 0 always. Notification hooks must never block Claude.
190
-
191
- **Opt out**: settings.json override or plugin disable.
192
-
193
- ---
194
-
195
179
  ### `auto-test-hooks.sh` — PostToolUse, matcher `Edit|Write`
196
180
 
197
181
  Triggers `test-hooks.sh` automatically when any `*/hooks/*.sh` file is edited. Catches accidental breakage of the plugin's own hooks (or your project's `.claude/hooks/` if you author your own) at edit time, before the broken hook runs against future tool calls.
@@ -240,13 +224,41 @@ After a `complete_round` MCP call succeeds, reconciles the round's `files_change
240
224
 
241
225
  ---
242
226
 
227
+ ### `cbp-cmux-workspace-sync.sh` — SessionStart, matcher `*`
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/`).
230
+
231
+ **Blocks vs warns**: never blocks — exit 0 on every path. A SessionStart hook must never prevent a session from opening.
232
+
233
+ **Skips when**: `$CMUX_WORKSPACE_ID` is unset (not running inside a cmux workspace). No-ops silently — cmux is an optional integration and repos without it are completely unaffected.
234
+
235
+ **cmux binary resolution order**: `$CMUX_BUNDLED_CLI_PATH` → `$CMUX_CLAUDE_HOOK_CMUX_BIN` → `cmux` on `$PATH`. All three are resolved by the `codebyplan cmux-sync` subcommand; the hook itself does not replicate this logic.
236
+
237
+ **Opt out**: settings.json override removing this entry, or plugin disable.
238
+
239
+ ---
240
+
241
+ ### `cbp-cmux-branch-watch.sh` — PostToolUse, matcher `Bash`
242
+
243
+ After any Bash tool call that contains a `git checkout` or `git switch` invocation, syncs the active cmux workspace title to the current branch and the workspace description to the repo folder basename. The match is broad — it also fires on file-restore forms like `git checkout -- <file>` — but `codebyplan cmux-sync` is idempotent, so a redundant sync on a non-branch-change is harmless, and a real branch change is never missed.
244
+
245
+ **Blocks vs warns**: never blocks — exit 0 on every path. PostToolUse hooks must never abort Claude.
246
+
247
+ **Skips when**: `$CMUX_WORKSPACE_ID` is unset; or the Bash command contains no `git checkout` / `git switch`; or `jq` is not on `$PATH` (safe parse failure → exit 0). Outside cmux or on commands without checkout/switch, the hook exits immediately with no work done.
248
+
249
+ **cmux binary resolution order**: same as `cbp-cmux-workspace-sync.sh` — delegated to `codebyplan cmux-sync`.
250
+
251
+ **Opt out**: settings.json override removing the PostToolUse Bash entry for this hook, or plugin disable.
252
+
253
+ ---
254
+
243
255
  ## Supporting (not registered)
244
256
 
245
257
  ### `test-hooks.sh` — invoked by `auto-test-hooks.sh`
246
258
 
247
- Test suite for the plugin's 10 registered hooks. Runs two passes:
259
+ Test suite for the plugin's 11 registered hooks. Runs two passes:
248
260
 
249
- 1. **Header check** — every registered hook (`lint-format-on-edit`, `test-coverage-gate`, `pre-commit-quality-gate`, `maestro-yaml-validate`, `notify`, `auto-test-hooks`, `mcp-migration-guard`, `validate-git-stash-deny`, `cbp-mcp-round-sync`) carries the required `# Hook:` and `# Purpose:` header comments. `statusline` uses its own `# Claude Code Status Line` marker.
261
+ 1. **Header check** — every registered hook (`lint-format-on-edit`, `test-coverage-gate`, `pre-commit-quality-gate`, `maestro-yaml-validate`, `auto-test-hooks`, `mcp-migration-guard`, `validate-git-stash-deny`, `cbp-mcp-round-sync`, `cbp-cmux-workspace-sync`, `cbp-cmux-branch-watch`) carries the required `# Hook:` and `# Purpose:` header comments. `statusline` uses its own `# Claude Code Status Line` marker.
250
262
  2. **Functional smoke tests** — each hook is invoked with synthetic stdin matching its fast-path / graceful-degrade input; all must exit 0.
251
263
 
252
264
  Not in `hooks.json` — invoked indirectly via `auto-test-hooks.sh` on hook edits, or directly via `bash ${CLAUDE_PLUGIN_ROOT}/hooks/test-hooks.sh`.
@@ -0,0 +1,39 @@
1
+ #!/bin/bash
2
+ # @scope: org-shared
3
+ # Hook: PostToolUse Bash
4
+ # Purpose: After a Bash tool call that contains a git checkout or git switch command,
5
+ # sync the active cmux workspace title to the current git branch and the
6
+ # workspace description to the repo folder basename.
7
+ # Delegates entirely to `codebyplan cmux-sync` — no cmux or git logic here.
8
+ # Matching is broad: it also fires on file-restore forms such as
9
+ # `git checkout -- <file>`. That is intentional — `codebyplan cmux-sync` is
10
+ # idempotent, so a redundant sync on a non-branch-change is harmless, and the
11
+ # broad match guarantees a real branch change is never missed.
12
+ # No-ops silently when CMUX_WORKSPACE_ID is unset or the command contains no
13
+ # checkout/switch. Exit 0 on every path — never blocks tool execution.
14
+
15
+ # Fast-path: skip npx spawn when clearly not in a cmux workspace.
16
+ [ -n "$CMUX_WORKSPACE_ID" ] || exit 0
17
+
18
+ # Parse stdin — PostToolUse hooks receive JSON on stdin.
19
+ # Guard against jq absence: if jq is not available the whole block is skipped.
20
+ if command -v jq >/dev/null 2>&1; then
21
+ INPUT=$(cat 2>/dev/null || true)
22
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
23
+
24
+ # Run the sync when the command contains git checkout/switch. Broad match by design:
25
+ # also catches file-restore forms (e.g. `git checkout -- <file>`); cmux-sync is
26
+ # idempotent so a redundant sync is harmless, and a real branch change is never missed.
27
+ if ! echo "$CMD" | grep -qE 'git[[:space:]]+(checkout|switch)' 2>/dev/null; then
28
+ exit 0
29
+ fi
30
+ else
31
+ # jq not available — drain stdin and skip.
32
+ cat >/dev/null 2>&1 || true
33
+ exit 0
34
+ fi
35
+
36
+ # Delegate to the CLI subcommand (self-no-ops when cmux binary is absent).
37
+ npx codebyplan cmux-sync >/dev/null 2>&1 || true
38
+
39
+ exit 0
@@ -0,0 +1,19 @@
1
+ #!/bin/bash
2
+ # @scope: org-shared
3
+ # Hook: SessionStart
4
+ # Purpose: On session start, sync the active cmux workspace title to the current git branch
5
+ # and the workspace description to the repo folder basename.
6
+ # Delegates entirely to `codebyplan cmux-sync` — no cmux or git logic here.
7
+ # No-ops silently when CMUX_WORKSPACE_ID is unset (not inside a cmux workspace).
8
+ # Exit 0 on every path — never blocks session start.
9
+
10
+ # Fast-path: skip npx spawn when clearly not in a cmux workspace.
11
+ [ -n "$CMUX_WORKSPACE_ID" ] || exit 0
12
+
13
+ # Drain stdin — SessionStart hooks receive JSON on stdin; we don't use it.
14
+ cat >/dev/null 2>&1
15
+
16
+ # Delegate to the CLI subcommand (self-no-ops when cmux binary is absent).
17
+ npx codebyplan cmux-sync >/dev/null 2>&1 || true
18
+
19
+ exit 0
@@ -62,7 +62,7 @@ for hook_file in "$HOOKS_DIR"/*.sh; do
62
62
 
63
63
  # Check for header comments (required for documentation generation).
64
64
  # Accept either marker convention used across the plugin's hooks:
65
- # - "# Hook:" + "# Purpose:" (used by notify, auto-test-hooks, etc.)
65
+ # - "# Hook:" + "# Purpose:" (used by auto-test-hooks, etc.)
66
66
  # - "# @event:" + a description line (used by maestro-yaml-validate, etc.)
67
67
  if (grep -q '^# Hook:' "$hook_file" && grep -q '^# Purpose:' "$hook_file") \
68
68
  || grep -q '^# @event:' "$hook_file"; then
@@ -77,14 +77,6 @@ echo ""
77
77
  # ===== FUNCTIONAL SMOKE TESTS =====
78
78
  echo "## Functional Smoke Tests"
79
79
 
80
- # cbp-notify.sh — graceful-degrade: exit 0 whether or not terminal-notifier is installed
81
- ACTUAL_EXIT=$(echo '{"message":"test","cwd":"/tmp"}' | bash "$HOOKS_DIR/cbp-notify.sh" >/dev/null 2>&1; echo $?)
82
- if [ "$ACTUAL_EXIT" = "0" ]; then
83
- test_result "cbp-notify.sh graceful-degrade exits 0" "passed" "passed"
84
- else
85
- test_result "cbp-notify.sh graceful-degrade exits 0" "passed" "failed"
86
- fi
87
-
88
80
  # cbp-lint-format-on-edit.sh — graceful-degrade: CLAUDE_PROJECT_DIR unset → exit 0
89
81
  if [ ! -f "$HOOKS_DIR/cbp-lint-format-on-edit.sh" ]; then
90
82
  test_result "cbp-lint-format-on-edit.sh present" "passed" "missing"
@@ -143,8 +135,8 @@ fi
143
135
 
144
136
  echo ""
145
137
 
146
- # ===== NEW HOOK SMOKE TESTS =====
147
- echo "## New Hook Smoke Tests (CHK-131)"
138
+ # ===== HOOK SMOKE TESTS — validate-git-stash-deny + cbp-mcp-round-sync =====
139
+ echo "## Hook Smoke Tests — validate-git-stash-deny + cbp-mcp-round-sync (CHK-131)"
148
140
 
149
141
  # --- validate-git-stash-deny.sh ---
150
142
 
@@ -263,6 +255,77 @@ fi
263
255
 
264
256
  echo ""
265
257
 
258
+ # ===== HOOK SMOKE TESTS — cbp-cmux-workspace-sync + cbp-cmux-branch-watch =====
259
+ echo "## Hook Smoke Tests — cbp-cmux-workspace-sync + cbp-cmux-branch-watch (CHK-162)"
260
+
261
+ # --- cbp-cmux-workspace-sync.sh ---
262
+
263
+ if [ ! -f "$HOOKS_DIR/cbp-cmux-workspace-sync.sh" ]; then
264
+ test_result "cbp-cmux-workspace-sync.sh present" "passed" "missing"
265
+ else
266
+ test_result "cbp-cmux-workspace-sync.sh present" "passed" "passed"
267
+
268
+ FIRST_LINE=$(head -1 "$HOOKS_DIR/cbp-cmux-workspace-sync.sh")
269
+ if echo "$FIRST_LINE" | grep -q '^#!/'; then
270
+ test_result "cbp-cmux-workspace-sync.sh has shebang" "passed" "passed"
271
+ else
272
+ test_result "cbp-cmux-workspace-sync.sh has shebang" "passed" "missing"
273
+ fi
274
+
275
+ if grep -q '@scope: org-shared' "$HOOKS_DIR/cbp-cmux-workspace-sync.sh"; then
276
+ test_result "cbp-cmux-workspace-sync.sh has @scope: org-shared" "passed" "passed"
277
+ else
278
+ test_result "cbp-cmux-workspace-sync.sh has @scope: org-shared" "passed" "missing"
279
+ fi
280
+
281
+ # Graceful-degrade: CMUX_WORKSPACE_ID unset → fast-path exit 0, no output
282
+ ACTUAL_EXIT=$(env -u CMUX_WORKSPACE_ID bash "$HOOKS_DIR/cbp-cmux-workspace-sync.sh" <<< '{}' >/dev/null 2>&1; echo $?)
283
+ if [ "$ACTUAL_EXIT" = "0" ]; then
284
+ test_result "cbp-cmux-workspace-sync.sh graceful-degrade (no CMUX_WORKSPACE_ID) exits 0" "passed" "passed"
285
+ else
286
+ test_result "cbp-cmux-workspace-sync.sh graceful-degrade (no CMUX_WORKSPACE_ID) exits 0" "passed" "failed"
287
+ fi
288
+ fi
289
+
290
+ # --- cbp-cmux-branch-watch.sh ---
291
+
292
+ if [ ! -f "$HOOKS_DIR/cbp-cmux-branch-watch.sh" ]; then
293
+ test_result "cbp-cmux-branch-watch.sh present" "passed" "missing"
294
+ else
295
+ test_result "cbp-cmux-branch-watch.sh present" "passed" "passed"
296
+
297
+ FIRST_LINE=$(head -1 "$HOOKS_DIR/cbp-cmux-branch-watch.sh")
298
+ if echo "$FIRST_LINE" | grep -q '^#!/'; then
299
+ test_result "cbp-cmux-branch-watch.sh has shebang" "passed" "passed"
300
+ else
301
+ test_result "cbp-cmux-branch-watch.sh has shebang" "passed" "missing"
302
+ fi
303
+
304
+ if grep -q '@scope: org-shared' "$HOOKS_DIR/cbp-cmux-branch-watch.sh"; then
305
+ test_result "cbp-cmux-branch-watch.sh has @scope: org-shared" "passed" "passed"
306
+ else
307
+ test_result "cbp-cmux-branch-watch.sh has @scope: org-shared" "passed" "missing"
308
+ fi
309
+
310
+ # Graceful-degrade: CMUX_WORKSPACE_ID unset → fast-path exit 0
311
+ ACTUAL_EXIT=$(env -u CMUX_WORKSPACE_ID bash "$HOOKS_DIR/cbp-cmux-branch-watch.sh" <<< '{"tool_input":{"command":"git checkout main"}}' >/dev/null 2>&1; echo $?)
312
+ if [ "$ACTUAL_EXIT" = "0" ]; then
313
+ test_result "cbp-cmux-branch-watch.sh graceful-degrade (no CMUX_WORKSPACE_ID) exits 0" "passed" "passed"
314
+ else
315
+ test_result "cbp-cmux-branch-watch.sh graceful-degrade (no CMUX_WORKSPACE_ID) exits 0" "passed" "failed"
316
+ fi
317
+
318
+ # Non-checkout command → no-op exit 0 (even with CMUX_WORKSPACE_ID set, non-checkout should skip sync)
319
+ ACTUAL_EXIT=$(CMUX_WORKSPACE_ID=test-ws bash "$HOOKS_DIR/cbp-cmux-branch-watch.sh" <<< '{"tool_input":{"command":"git status"}}' >/dev/null 2>&1; echo $?)
320
+ if [ "$ACTUAL_EXIT" = "0" ]; then
321
+ test_result "cbp-cmux-branch-watch.sh non-checkout command exits 0" "passed" "passed"
322
+ else
323
+ test_result "cbp-cmux-branch-watch.sh non-checkout command exits 0" "passed" "failed"
324
+ fi
325
+ fi
326
+
327
+ echo ""
328
+
266
329
  # ===== SUMMARY =====
267
330
  echo "=== TEST SUMMARY ==="
268
331
  echo -e "Passed: ${GREEN}$PASSED${NC}"
@@ -1,5 +1,16 @@
1
1
  {
2
2
  "hooks": {
3
+ "SessionStart": [
4
+ {
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-cmux-workspace-sync.sh"
10
+ }
11
+ ]
12
+ }
13
+ ],
3
14
  "PreToolUse": [
4
15
  {
5
16
  "matcher": "Edit|Write|MultiEdit",
@@ -59,15 +70,13 @@
59
70
  "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-mcp-round-sync.sh"
60
71
  }
61
72
  ]
62
- }
63
- ],
64
- "Notification": [
73
+ },
65
74
  {
66
- "matcher": "*",
75
+ "matcher": "Bash",
67
76
  "hooks": [
68
77
  {
69
78
  "type": "command",
70
- "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-notify.sh"
79
+ "command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-cmux-branch-watch.sh"
71
80
  }
72
81
  ]
73
82
  }
@@ -176,6 +176,8 @@
176
176
  "Bash(npx codebyplan whoami:*)",
177
177
  "Bash(codebyplan resolve-worktree:*)",
178
178
  "Bash(npx codebyplan resolve-worktree:*)",
179
+ "Bash(codebyplan cmux-sync:*)",
180
+ "Bash(npx codebyplan cmux-sync:*)",
179
181
  "Bash(codebyplan statusline:*)",
180
182
  "Bash(npx codebyplan statusline:*)",
181
183
  "Bash(codebyplan ports:*)",
@@ -181,13 +181,14 @@ command + which apps still need their `eslint.config.mjs` generated.
181
181
  `reference/*.md`, never silently skip
182
182
  - `codebyplan eslint init` failure is non-fatal — print the manual command and still write eslint.json
183
183
  - Atomic write (tmp + mv) — never leave eslint.json partial
184
- - Reference docs track the **latest official** flat-config setup and flag where CBP's DB
185
- presets diverge (e.g. ESLint v10 + `eslint-plugin-react-hooks@7` bundled compiler rules)
184
+ - Reference docs contain **only the official upstream ESLint setup** per stack (verbatim from each
185
+ tool's own docs) they are not CBP preset opinions. `codebyplan eslint init` generates from the DB
186
+ presets, which may differ; the reference docs are the canonical official guidance
186
187
 
187
188
  ## Additional resources
188
189
 
189
190
  - TypeScript foundation (ESLint v10 flat config): [reference/base.md](reference/base.md)
190
- - Next.js: [reference/nextjs.md](reference/nextjs.md) · React (+ Storybook): [reference/react.md](reference/react.md)
191
+ - Next.js: [reference/nextjs.md](reference/nextjs.md) · React: [reference/react.md](reference/react.md)
191
192
  - Node / Hono / Express: [reference/node.md](reference/node.md) · NestJS: [reference/nestjs.md](reference/nestjs.md)
192
193
  - CLI tools: [reference/cli.md](reference/cli.md) · Tailwind CSS: [reference/tailwind.md](reference/tailwind.md)
193
194
  - React Native / Expo: [reference/react-native.md](reference/react-native.md)