codebyplan 1.13.4 → 1.13.6
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 +40 -5
- package/dist/cli.js +102 -29
- package/package.json +1 -1
- package/templates/hooks/README.md +31 -3
- package/templates/hooks/cbp-cmux-branch-watch.sh +39 -0
- package/templates/hooks/cbp-cmux-workspace-sync.sh +19 -0
- package/templates/hooks/cbp-test-hooks.sh +73 -2
- package/templates/hooks/hooks.json +20 -0
- package/templates/settings.project.base.json +2 -0
- package/templates/skills/cbp-git-worktree-create/SKILL.md +5 -8
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ npx codebyplan setup
|
|
|
10
10
|
|
|
11
11
|
This will:
|
|
12
12
|
|
|
13
|
-
1.
|
|
13
|
+
1. Sign you in via OAuth (a browser window opens automatically; no API key needed)
|
|
14
14
|
2. Configure Claude Code to connect via remote MCP
|
|
15
15
|
3. Optionally link a repository
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@ This will:
|
|
|
18
18
|
|
|
19
19
|
### `codebyplan setup`
|
|
20
20
|
|
|
21
|
-
Interactive setup wizard.
|
|
21
|
+
Interactive setup wizard. Authenticates via OAuth and configures the MCP server connection.
|
|
22
22
|
|
|
23
23
|
### `codebyplan config`
|
|
24
24
|
|
|
@@ -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:
|
|
@@ -95,14 +129,15 @@ Claude Code connects to CodeByPlan via a remote MCP server. The `setup` command
|
|
|
95
129
|
{
|
|
96
130
|
"mcpServers": {
|
|
97
131
|
"codebyplan": {
|
|
98
|
-
"
|
|
99
|
-
"
|
|
132
|
+
"type": "http",
|
|
133
|
+
"url": "https://mcp.codebyplan.com/mcp"
|
|
100
134
|
}
|
|
101
135
|
}
|
|
102
136
|
}
|
|
103
137
|
```
|
|
104
138
|
|
|
139
|
+
Authentication is handled via OAuth Bearer — no API key is stored in this file.
|
|
140
|
+
|
|
105
141
|
## Learn More
|
|
106
142
|
|
|
107
143
|
- [codebyplan.com](https://codebyplan.com)
|
|
108
|
-
- [API Keys](https://codebyplan.com/settings/api-keys)
|
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.
|
|
17
|
+
VERSION = "1.13.6";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -148,8 +148,7 @@ var init_gitignore_block = __esm({
|
|
|
148
148
|
".claude/scheduled_tasks.lock",
|
|
149
149
|
".codebyplan/device.local.json",
|
|
150
150
|
".codebyplan/statusline.local.json",
|
|
151
|
-
".codebyplan.local.json"
|
|
152
|
-
".mcp.json"
|
|
151
|
+
".codebyplan.local.json"
|
|
153
152
|
];
|
|
154
153
|
GITIGNORE_BLOCK_START = "# >>> codebyplan (managed) >>>";
|
|
155
154
|
GITIGNORE_BLOCK_END = "# <<< codebyplan <<<";
|
|
@@ -1230,7 +1229,7 @@ async function readConfig(path8) {
|
|
|
1230
1229
|
}
|
|
1231
1230
|
}
|
|
1232
1231
|
function buildMcpEntry() {
|
|
1233
|
-
return { url: mcpEndpoint() };
|
|
1232
|
+
return { type: "http", url: mcpEndpoint() };
|
|
1234
1233
|
}
|
|
1235
1234
|
async function writeMcpConfig(scope) {
|
|
1236
1235
|
const configPath = getConfigPath(scope);
|
|
@@ -1391,11 +1390,6 @@ async function runSetup() {
|
|
|
1391
1390
|
const configPath = await writeMcpConfig(scope);
|
|
1392
1391
|
console.log(` Done! Config written to ${configPath}
|
|
1393
1392
|
`);
|
|
1394
|
-
if (auth.kind === "legacy" && scope === "project") {
|
|
1395
|
-
console.log(
|
|
1396
|
-
" Note: .mcp.json contains your API key \u2014 add it to .gitignore.\n"
|
|
1397
|
-
);
|
|
1398
|
-
}
|
|
1399
1393
|
let repos = [];
|
|
1400
1394
|
try {
|
|
1401
1395
|
repos = await fetchRepos(auth);
|
|
@@ -1449,8 +1443,8 @@ async function runSetup() {
|
|
|
1449
1443
|
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
1450
1444
|
let branch = "main";
|
|
1451
1445
|
try {
|
|
1452
|
-
const { execSync:
|
|
1453
|
-
branch =
|
|
1446
|
+
const { execSync: execSync7 } = await import("node:child_process");
|
|
1447
|
+
branch = execSync7("git symbolic-ref --short HEAD", {
|
|
1454
1448
|
cwd: projectPath,
|
|
1455
1449
|
encoding: "utf-8"
|
|
1456
1450
|
}).trim();
|
|
@@ -1782,8 +1776,9 @@ async function rewriteConfig(path8, config, newUrl) {
|
|
|
1782
1776
|
if (!servers) return false;
|
|
1783
1777
|
const entry = servers.codebyplan;
|
|
1784
1778
|
if (!entry) return false;
|
|
1785
|
-
if (!entryHasLegacyApiKey(entry) && entry.url === newUrl
|
|
1786
|
-
|
|
1779
|
+
if (!entryHasLegacyApiKey(entry) && entry.url === newUrl && entry.type === "http")
|
|
1780
|
+
return false;
|
|
1781
|
+
servers.codebyplan = { type: "http", url: newUrl };
|
|
1787
1782
|
await writeFile7(path8, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
1788
1783
|
return true;
|
|
1789
1784
|
}
|
|
@@ -1803,7 +1798,7 @@ async function runUpgradeAuth() {
|
|
|
1803
1798
|
}
|
|
1804
1799
|
if (migrated === 0) {
|
|
1805
1800
|
console.log(
|
|
1806
|
-
"
|
|
1801
|
+
" All codebyplan MCP entries are already up to date \u2014 nothing to change."
|
|
1807
1802
|
);
|
|
1808
1803
|
} else {
|
|
1809
1804
|
console.log(
|
|
@@ -3069,9 +3064,9 @@ async function eslintInit(repoId, projectPath) {
|
|
|
3069
3064
|
Install ${missingPkgs.length} missing packages? [Y/n] `
|
|
3070
3065
|
);
|
|
3071
3066
|
if (confirmed) {
|
|
3072
|
-
const { execSync:
|
|
3067
|
+
const { execSync: execSync7 } = await import("node:child_process");
|
|
3073
3068
|
try {
|
|
3074
|
-
|
|
3069
|
+
execSync7(installCmd, { cwd: projectPath, stdio: "inherit" });
|
|
3075
3070
|
console.log(" Packages installed.\n");
|
|
3076
3071
|
} catch (err) {
|
|
3077
3072
|
console.error(
|
|
@@ -5586,10 +5581,25 @@ var init_scaffold_publish_workflow2 = __esm({
|
|
|
5586
5581
|
}
|
|
5587
5582
|
});
|
|
5588
5583
|
|
|
5584
|
+
// src/cli/process-exit-signal.ts
|
|
5585
|
+
var ProcessExitSignal;
|
|
5586
|
+
var init_process_exit_signal = __esm({
|
|
5587
|
+
"src/cli/process-exit-signal.ts"() {
|
|
5588
|
+
"use strict";
|
|
5589
|
+
ProcessExitSignal = class extends Error {
|
|
5590
|
+
code;
|
|
5591
|
+
constructor(code) {
|
|
5592
|
+
super(`process.exit(${code})`);
|
|
5593
|
+
this.name = "ProcessExitSignal";
|
|
5594
|
+
this.code = code;
|
|
5595
|
+
}
|
|
5596
|
+
};
|
|
5597
|
+
}
|
|
5598
|
+
});
|
|
5599
|
+
|
|
5589
5600
|
// src/cli/resolve-worktree.ts
|
|
5590
5601
|
var resolve_worktree_exports = {};
|
|
5591
5602
|
__export(resolve_worktree_exports, {
|
|
5592
|
-
ProcessExitSignal: () => ProcessExitSignal,
|
|
5593
5603
|
runResolveWorktree: () => runResolveWorktree
|
|
5594
5604
|
});
|
|
5595
5605
|
import { execSync as execSync5 } from "node:child_process";
|
|
@@ -5713,21 +5723,78 @@ function emitAndExit(worktreeId, errorContext, jsonMode) {
|
|
|
5713
5723
|
}
|
|
5714
5724
|
process.exit(0);
|
|
5715
5725
|
}
|
|
5716
|
-
var ProcessExitSignal;
|
|
5717
5726
|
var init_resolve_worktree2 = __esm({
|
|
5718
5727
|
"src/cli/resolve-worktree.ts"() {
|
|
5719
5728
|
"use strict";
|
|
5720
5729
|
init_flags();
|
|
5721
5730
|
init_local_config();
|
|
5722
5731
|
init_resolve_worktree();
|
|
5723
|
-
|
|
5724
|
-
|
|
5725
|
-
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
5732
|
+
init_process_exit_signal();
|
|
5733
|
+
}
|
|
5734
|
+
});
|
|
5735
|
+
|
|
5736
|
+
// src/cli/cmux-sync.ts
|
|
5737
|
+
var cmux_sync_exports = {};
|
|
5738
|
+
__export(cmux_sync_exports, {
|
|
5739
|
+
runCmuxSync: () => runCmuxSync
|
|
5740
|
+
});
|
|
5741
|
+
import { execSync as execSync6, execFileSync } from "node:child_process";
|
|
5742
|
+
import { basename } from "node:path";
|
|
5743
|
+
async function runCmuxSync() {
|
|
5744
|
+
try {
|
|
5745
|
+
if (!process.env.CMUX_WORKSPACE_ID) {
|
|
5746
|
+
process.exit(0);
|
|
5747
|
+
}
|
|
5748
|
+
const bin = process.env.CMUX_BUNDLED_CLI_PATH || process.env.CMUX_CLAUDE_HOOK_CMUX_BIN || "cmux";
|
|
5749
|
+
let branch = "";
|
|
5750
|
+
try {
|
|
5751
|
+
branch = execSync6("git rev-parse --abbrev-ref HEAD", {
|
|
5752
|
+
encoding: "utf8"
|
|
5753
|
+
}).trim();
|
|
5754
|
+
} catch {
|
|
5755
|
+
}
|
|
5756
|
+
let folder = "";
|
|
5757
|
+
try {
|
|
5758
|
+
const toplevel = execSync6("git rev-parse --show-toplevel", {
|
|
5759
|
+
encoding: "utf8"
|
|
5760
|
+
}).trim();
|
|
5761
|
+
folder = basename(toplevel);
|
|
5762
|
+
} catch {
|
|
5763
|
+
}
|
|
5764
|
+
if (branch) {
|
|
5765
|
+
try {
|
|
5766
|
+
execFileSync(bin, [
|
|
5767
|
+
"workspace-action",
|
|
5768
|
+
"--action",
|
|
5769
|
+
"rename",
|
|
5770
|
+
"--title",
|
|
5771
|
+
branch
|
|
5772
|
+
]);
|
|
5773
|
+
} catch {
|
|
5729
5774
|
}
|
|
5730
|
-
}
|
|
5775
|
+
}
|
|
5776
|
+
if (folder) {
|
|
5777
|
+
try {
|
|
5778
|
+
execFileSync(bin, [
|
|
5779
|
+
"workspace-action",
|
|
5780
|
+
"--action",
|
|
5781
|
+
"set-description",
|
|
5782
|
+
"--description",
|
|
5783
|
+
folder
|
|
5784
|
+
]);
|
|
5785
|
+
} catch {
|
|
5786
|
+
}
|
|
5787
|
+
}
|
|
5788
|
+
process.exit(0);
|
|
5789
|
+
} catch (err) {
|
|
5790
|
+
if (err instanceof ProcessExitSignal) throw err;
|
|
5791
|
+
process.exit(0);
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
var init_cmux_sync = __esm({
|
|
5795
|
+
"src/cli/cmux-sync.ts"() {
|
|
5796
|
+
"use strict";
|
|
5797
|
+
init_process_exit_signal();
|
|
5731
5798
|
}
|
|
5732
5799
|
});
|
|
5733
5800
|
|
|
@@ -6009,8 +6076,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
6009
6076
|
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
6010
6077
|
let branch = "main";
|
|
6011
6078
|
try {
|
|
6012
|
-
const { execSync:
|
|
6013
|
-
branch =
|
|
6079
|
+
const { execSync: execSync7 } = await import("node:child_process");
|
|
6080
|
+
branch = execSync7("git symbolic-ref --short HEAD", {
|
|
6014
6081
|
cwd: projectPath,
|
|
6015
6082
|
encoding: "utf-8"
|
|
6016
6083
|
}).trim();
|
|
@@ -6555,10 +6622,10 @@ async function runTechStack() {
|
|
|
6555
6622
|
);
|
|
6556
6623
|
}
|
|
6557
6624
|
try {
|
|
6558
|
-
const { execSync:
|
|
6625
|
+
const { execSync: execSync7 } = await import("node:child_process");
|
|
6559
6626
|
let branch = "main";
|
|
6560
6627
|
try {
|
|
6561
|
-
branch =
|
|
6628
|
+
branch = execSync7("git symbolic-ref --short HEAD", {
|
|
6562
6629
|
cwd: projectPath,
|
|
6563
6630
|
encoding: "utf-8"
|
|
6564
6631
|
}).trim();
|
|
@@ -7467,6 +7534,11 @@ void (async () => {
|
|
|
7467
7534
|
await runResolveWorktree2();
|
|
7468
7535
|
process.exit(0);
|
|
7469
7536
|
}
|
|
7537
|
+
if (arg === "cmux-sync") {
|
|
7538
|
+
const { runCmuxSync: runCmuxSync2 } = await Promise.resolve().then(() => (init_cmux_sync(), cmux_sync_exports));
|
|
7539
|
+
await runCmuxSync2();
|
|
7540
|
+
process.exit(0);
|
|
7541
|
+
}
|
|
7470
7542
|
if (arg === "config") {
|
|
7471
7543
|
const { runConfig: runConfig2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
7472
7544
|
await runConfig2();
|
|
@@ -7565,6 +7637,7 @@ void (async () => {
|
|
|
7565
7637
|
codebyplan claude Claude asset management (install/update/uninstall)
|
|
7566
7638
|
codebyplan statusline Show or set the statusline renderer (bash/node/python)
|
|
7567
7639
|
codebyplan resolve-worktree Resolve active worktree UUID from device+path+branch tuple
|
|
7640
|
+
codebyplan cmux-sync Sync cmux workspace title/description to current git branch and repo folder
|
|
7568
7641
|
codebyplan help Show this help message
|
|
7569
7642
|
codebyplan --version Print version
|
|
7570
7643
|
|
package/package.json
CHANGED
|
@@ -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 and PostToolUse events are wired. (`Notification`, `
|
|
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
|
|
|
@@ -224,13 +224,41 @@ After a `complete_round` MCP call succeeds, reconciles the round's `files_change
|
|
|
224
224
|
|
|
225
225
|
---
|
|
226
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
|
+
|
|
227
255
|
## Supporting (not registered)
|
|
228
256
|
|
|
229
257
|
### `test-hooks.sh` — invoked by `auto-test-hooks.sh`
|
|
230
258
|
|
|
231
|
-
Test suite for the plugin's
|
|
259
|
+
Test suite for the plugin's 11 registered hooks. Runs two passes:
|
|
232
260
|
|
|
233
|
-
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`) 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.
|
|
234
262
|
2. **Functional smoke tests** — each hook is invoked with synthetic stdin matching its fast-path / graceful-degrade input; all must exit 0.
|
|
235
263
|
|
|
236
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
|
|
@@ -135,8 +135,8 @@ fi
|
|
|
135
135
|
|
|
136
136
|
echo ""
|
|
137
137
|
|
|
138
|
-
# =====
|
|
139
|
-
echo "##
|
|
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)"
|
|
140
140
|
|
|
141
141
|
# --- validate-git-stash-deny.sh ---
|
|
142
142
|
|
|
@@ -255,6 +255,77 @@ fi
|
|
|
255
255
|
|
|
256
256
|
echo ""
|
|
257
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
|
+
|
|
258
329
|
# ===== SUMMARY =====
|
|
259
330
|
echo "=== TEST SUMMARY ==="
|
|
260
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,6 +70,15 @@
|
|
|
59
70
|
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-mcp-round-sync.sh"
|
|
60
71
|
}
|
|
61
72
|
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"matcher": "Bash",
|
|
76
|
+
"hooks": [
|
|
77
|
+
{
|
|
78
|
+
"type": "command",
|
|
79
|
+
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-cmux-branch-watch.sh"
|
|
80
|
+
}
|
|
81
|
+
]
|
|
62
82
|
}
|
|
63
83
|
]
|
|
64
84
|
}
|
|
@@ -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:*)",
|
|
@@ -113,7 +113,7 @@ git worktree add "$WORKTREE_PATH" "$BRANCH_NAME"
|
|
|
113
113
|
|
|
114
114
|
### Step 6: Set Up MCP Connection
|
|
115
115
|
|
|
116
|
-
Copy `.mcp.json` from the main repo to the worktree. The
|
|
116
|
+
Copy `.mcp.json` from the main repo to the worktree. The file is committed and contains only the public MCP URL (no secret), so this is a plain `cp` — no path rewriting or key substitution needed:
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
119
|
cp "$MAIN_REPO/.mcp.json" "$WORKTREE_PATH/.mcp.json"
|
|
@@ -125,10 +125,8 @@ Expected shape (do NOT rewrite paths — this is remote HTTP):
|
|
|
125
125
|
{
|
|
126
126
|
"mcpServers": {
|
|
127
127
|
"codebyplan": {
|
|
128
|
-
"
|
|
129
|
-
"
|
|
130
|
-
"x-api-key": "${CODEBYPLAN_API_KEY}"
|
|
131
|
-
}
|
|
128
|
+
"type": "http",
|
|
129
|
+
"url": "https://mcp.codebyplan.com/mcp"
|
|
132
130
|
}
|
|
133
131
|
}
|
|
134
132
|
}
|
|
@@ -136,7 +134,7 @@ Expected shape (do NOT rewrite paths — this is remote HTTP):
|
|
|
136
134
|
|
|
137
135
|
### Step 7: Set Up Environment
|
|
138
136
|
|
|
139
|
-
Copy `.env.local` from the main repo to the worktree (contains
|
|
137
|
+
Copy `.env.local` from the main repo to the worktree (contains the app's environment variables such as Supabase keys and feature flags — MCP auth is OAuth Bearer handled by Claude Code, not a key in this file):
|
|
140
138
|
|
|
141
139
|
```bash
|
|
142
140
|
cp "$MAIN_REPO/.env.local" "$WORKTREE_PATH/.env.local"
|
|
@@ -214,8 +212,7 @@ No need to mark as `skip-worktree` — the committed files are merge-safe per CH
|
|
|
214
212
|
**CodeByPlan**: Registered (worktree ID: [id])
|
|
215
213
|
|
|
216
214
|
### Setup
|
|
217
|
-
- MCP: connected (remote endpoint)
|
|
218
|
-
- API key: copied from main repo `.env.local`
|
|
215
|
+
- MCP: connected (remote endpoint, OAuth Bearer)
|
|
219
216
|
- `.claude/` files: full copy on disk (rules, skills, hooks, agents, context)
|
|
220
217
|
|
|
221
218
|
### Next Steps
|