codebyplan 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +50 -71
- package/package.json +1 -1
- package/templates/hooks/README.md +1 -13
- package/templates/hooks/cbp-test-coverage-gate.sh +8 -0
- package/templates/hooks/cbp-test-hooks.sh +0 -42
- package/templates/hooks/hooks.json +0 -9
- package/templates/skills/cbp-checkpoint-start/SKILL.md +2 -2
- package/templates/skills/cbp-session-start/SKILL.md +1 -1
- package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/SKILL.md +1 -1
- package/templates/skills/cbp-setup-eslint/SKILL.md +199 -0
- package/templates/skills/cbp-setup-eslint/reference/base.md +82 -0
- package/templates/skills/cbp-setup-eslint/reference/cli.md +56 -0
- package/templates/skills/cbp-setup-eslint/reference/e2e.md +68 -0
- package/templates/skills/cbp-setup-eslint/reference/jest.md +59 -0
- package/templates/skills/cbp-setup-eslint/reference/nestjs.md +69 -0
- package/templates/skills/cbp-setup-eslint/reference/nextjs.md +63 -0
- package/templates/skills/cbp-setup-eslint/reference/node.md +74 -0
- package/templates/skills/cbp-setup-eslint/reference/react-native.md +60 -0
- package/templates/skills/cbp-setup-eslint/reference/react.md +82 -0
- package/templates/skills/cbp-setup-eslint/reference/tailwind.md +64 -0
- package/templates/skills/cbp-setup-eslint/reference/testing-react.md +57 -0
- package/templates/skills/cbp-setup-eslint/reference/vitest.md +62 -0
- package/templates/skills/cbp-task-complete/SKILL.md +1 -3
- package/templates/skills/cbp-task-start/SKILL.md +3 -3
- package/templates/hooks/cbp-mcp-worktree-inject.sh +0 -76
- /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/maestro.md +0 -0
- /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/playwright.md +0 -0
- /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/tauri.md +0 -0
- /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/vscode.md +0 -0
- /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/xcuitest.md +0 -0
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.
|
|
17
|
+
VERSION = "1.13.0";
|
|
18
18
|
PACKAGE_NAME = "codebyplan";
|
|
19
19
|
}
|
|
20
20
|
});
|
|
@@ -978,28 +978,36 @@ var init_browser = __esm({
|
|
|
978
978
|
});
|
|
979
979
|
|
|
980
980
|
// src/oauth/device-flow.ts
|
|
981
|
-
async function initiateDeviceCode(clientId) {
|
|
981
|
+
async function initiateDeviceCode(clientId, scope, resource = "https://mcp.codebyplan.com/") {
|
|
982
|
+
const body = { client_id: clientId, resource };
|
|
983
|
+
if (scope !== void 0) {
|
|
984
|
+
body.scope = scope;
|
|
985
|
+
}
|
|
982
986
|
const res = await fetch(deviceEndpoint(), {
|
|
983
987
|
method: "POST",
|
|
984
988
|
headers: { "Content-Type": "application/json" },
|
|
985
|
-
body: JSON.stringify(
|
|
989
|
+
body: JSON.stringify(body),
|
|
986
990
|
signal: AbortSignal.timeout(1e4)
|
|
987
991
|
});
|
|
988
992
|
if (!res.ok) {
|
|
989
|
-
const
|
|
990
|
-
if (
|
|
993
|
+
const body2 = await res.json().catch(() => ({}));
|
|
994
|
+
if (body2.error === "invalid_client") {
|
|
991
995
|
throw new OAuthInvalidClientError();
|
|
992
996
|
}
|
|
993
|
-
const msg =
|
|
997
|
+
const msg = body2.error_description ?? body2.error ?? `HTTP ${res.status}`;
|
|
994
998
|
throw new Error(`Failed to initiate device flow: ${msg}`);
|
|
995
999
|
}
|
|
996
|
-
|
|
1000
|
+
const dcResponse = await res.json();
|
|
1001
|
+
return { ...dcResponse, resource };
|
|
997
1002
|
}
|
|
998
|
-
async function pollOnce(deviceCode, clientId) {
|
|
1003
|
+
async function pollOnce(deviceCode, clientId, resource) {
|
|
999
1004
|
const params = new URLSearchParams();
|
|
1000
1005
|
params.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
|
1001
1006
|
params.set("device_code", deviceCode);
|
|
1002
1007
|
params.set("client_id", clientId);
|
|
1008
|
+
if (resource !== void 0) {
|
|
1009
|
+
params.set("resource", resource);
|
|
1010
|
+
}
|
|
1003
1011
|
const res = await fetch(tokenEndpoint(), {
|
|
1004
1012
|
method: "POST",
|
|
1005
1013
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
@@ -1023,7 +1031,11 @@ async function pollUntilSettled(opts) {
|
|
|
1023
1031
|
const startMs = now();
|
|
1024
1032
|
const expiresAtMs = startMs + opts.expiresInSec * 1e3;
|
|
1025
1033
|
while (now() < expiresAtMs) {
|
|
1026
|
-
const outcome = await pollOnce(
|
|
1034
|
+
const outcome = await pollOnce(
|
|
1035
|
+
opts.deviceCode,
|
|
1036
|
+
opts.clientId,
|
|
1037
|
+
opts.resource
|
|
1038
|
+
);
|
|
1027
1039
|
if (outcome.kind !== "pending") return outcome;
|
|
1028
1040
|
if (opts.onTick) opts.onTick(Math.floor((now() - startMs) / 1e3));
|
|
1029
1041
|
await sleep2(opts.intervalSec * 1e3);
|
|
@@ -1126,10 +1138,11 @@ async function fetchEmail(accessToken) {
|
|
|
1126
1138
|
return null;
|
|
1127
1139
|
}
|
|
1128
1140
|
}
|
|
1129
|
-
async function runLogin() {
|
|
1141
|
+
async function runLogin(options = {}) {
|
|
1142
|
+
const scope = options.admin ? "mcp:read mcp:write mcp:admin" : "mcp:read mcp:write";
|
|
1130
1143
|
console.log("\n CodeByPlan login\n");
|
|
1131
1144
|
const { clientId, result: dc } = await ensureClientFor(
|
|
1132
|
-
(id) => initiateDeviceCode(id)
|
|
1145
|
+
(id) => initiateDeviceCode(id, scope)
|
|
1133
1146
|
);
|
|
1134
1147
|
const verificationUri = dc.verification_uri_complete ?? dc.verification_uri;
|
|
1135
1148
|
console.log(` Visit: ${verificationUri}`);
|
|
@@ -1143,7 +1156,8 @@ async function runLogin() {
|
|
|
1143
1156
|
deviceCode: dc.device_code,
|
|
1144
1157
|
clientId,
|
|
1145
1158
|
intervalSec: dc.interval,
|
|
1146
|
-
expiresInSec: dc.expires_in
|
|
1159
|
+
expiresInSec: dc.expires_in,
|
|
1160
|
+
resource: dc.resource
|
|
1147
1161
|
});
|
|
1148
1162
|
if (outcome.kind === "denied") {
|
|
1149
1163
|
console.log(" Denied. Authorization request was rejected.\n");
|
|
@@ -1338,6 +1352,11 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
|
|
|
1338
1352
|
JSON.stringify({}, null, 2) + "\n",
|
|
1339
1353
|
"utf-8"
|
|
1340
1354
|
);
|
|
1355
|
+
await writeFile6(
|
|
1356
|
+
join6(codebyplanDir, "eslint.json"),
|
|
1357
|
+
JSON.stringify({}, null, 2) + "\n",
|
|
1358
|
+
"utf-8"
|
|
1359
|
+
);
|
|
1341
1360
|
const statuslinePath = join6(codebyplanDir, "statusline.json");
|
|
1342
1361
|
let statuslineExists = false;
|
|
1343
1362
|
try {
|
|
@@ -1355,7 +1374,7 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
|
|
|
1355
1374
|
await writeLocalConfig(projectPath, { device_id: deviceId });
|
|
1356
1375
|
console.log(` Created ${codebyplanDir}/`);
|
|
1357
1376
|
console.log(
|
|
1358
|
-
` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, statusline.json`
|
|
1377
|
+
` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, eslint.json, statusline.json`
|
|
1359
1378
|
);
|
|
1360
1379
|
console.log(` device.local.json (gitignored)`);
|
|
1361
1380
|
const gitignoreAction = await ensureManagedGitignoreBlock(projectPath);
|
|
@@ -3472,7 +3491,7 @@ Run 'codebyplan round help' for usage.
|
|
|
3472
3491
|
}
|
|
3473
3492
|
function printRoundHelp() {
|
|
3474
3493
|
process.stdout.write(
|
|
3475
|
-
"\n codebyplan round <subcommand>\n\n Subcommands:\n sync-approvals Sync git diff and approvals with round/task state\n\n sync-approvals flags:\n --round-id <uuid> Round UUID (required)\n --task-id <uuid> Task UUID (required)\n --
|
|
3494
|
+
"\n codebyplan round <subcommand>\n\n Subcommands:\n sync-approvals Sync git diff and approvals with round/task state\n\n sync-approvals flags:\n --round-id <uuid> Round UUID (required)\n --task-id <uuid> Task UUID (required)\n --dry-run Print merged payload to stdout without writing\n\n"
|
|
3476
3495
|
);
|
|
3477
3496
|
}
|
|
3478
3497
|
function parseFlagsFromArgs(args) {
|
|
@@ -3513,10 +3532,6 @@ async function runRoundSyncApprovals(args) {
|
|
|
3513
3532
|
let skipReason = null;
|
|
3514
3533
|
let stdoutPayload = null;
|
|
3515
3534
|
try {
|
|
3516
|
-
let callerWorktreeId = flags["worktree-id"];
|
|
3517
|
-
if (!callerWorktreeId) {
|
|
3518
|
-
callerWorktreeId = await autoResolveWorktreeId();
|
|
3519
|
-
}
|
|
3520
3535
|
const found = await findCodebyplanConfig(process.cwd());
|
|
3521
3536
|
const repoRoot = found ? (
|
|
3522
3537
|
// Walk up to the directory containing .codebyplan/ or .codebyplan.json
|
|
@@ -3583,23 +3598,15 @@ async function runRoundSyncApprovals(args) {
|
|
|
3583
3598
|
2
|
|
3584
3599
|
);
|
|
3585
3600
|
} else {
|
|
3586
|
-
|
|
3601
|
+
await mcpCall("update_round", {
|
|
3587
3602
|
round_id: roundId,
|
|
3588
3603
|
files_changed: result.merged_files_changed
|
|
3589
|
-
};
|
|
3590
|
-
|
|
3591
|
-
roundArgs["caller_worktree_id"] = callerWorktreeId;
|
|
3592
|
-
}
|
|
3593
|
-
await mcpCall("update_round", roundArgs);
|
|
3594
|
-
const taskArgs = {
|
|
3604
|
+
});
|
|
3605
|
+
await mcpCall("update_task", {
|
|
3595
3606
|
task_id: taskId,
|
|
3596
3607
|
files_changed: result.merged_files_changed,
|
|
3597
3608
|
app_file_approval_by_user: false
|
|
3598
|
-
};
|
|
3599
|
-
if (callerWorktreeId) {
|
|
3600
|
-
taskArgs["caller_worktree_id"] = callerWorktreeId;
|
|
3601
|
-
}
|
|
3602
|
-
await mcpCall("update_task", taskArgs);
|
|
3609
|
+
});
|
|
3603
3610
|
stdoutPayload = JSON.stringify(
|
|
3604
3611
|
{
|
|
3605
3612
|
added: result.added,
|
|
@@ -3633,42 +3640,6 @@ async function runRoundSyncApprovals(args) {
|
|
|
3633
3640
|
process.stdout.write(stdoutPayload + "\n");
|
|
3634
3641
|
process.exit(0);
|
|
3635
3642
|
}
|
|
3636
|
-
async function autoResolveWorktreeId() {
|
|
3637
|
-
try {
|
|
3638
|
-
const projectPath = process.cwd();
|
|
3639
|
-
const found = await findCodebyplanConfig(projectPath);
|
|
3640
|
-
if (!found?.contents.repo_id) return void 0;
|
|
3641
|
-
const repoId = found.contents.repo_id;
|
|
3642
|
-
const deviceId = await getOrCreateDeviceId(projectPath);
|
|
3643
|
-
let branch = "";
|
|
3644
|
-
try {
|
|
3645
|
-
branch = execSync2("git symbolic-ref --short HEAD", {
|
|
3646
|
-
cwd: projectPath,
|
|
3647
|
-
encoding: "utf-8"
|
|
3648
|
-
}).trim();
|
|
3649
|
-
} catch {
|
|
3650
|
-
}
|
|
3651
|
-
const worktreeId = await resolveWorktreeId({
|
|
3652
|
-
repoId,
|
|
3653
|
-
repoPath: projectPath,
|
|
3654
|
-
branch,
|
|
3655
|
-
deviceId
|
|
3656
|
-
});
|
|
3657
|
-
if (worktreeId) return worktreeId;
|
|
3658
|
-
const fallbackId = await resolveWorktreeByBranch({
|
|
3659
|
-
repoId,
|
|
3660
|
-
deviceId,
|
|
3661
|
-
branch
|
|
3662
|
-
});
|
|
3663
|
-
if (fallbackId) return fallbackId;
|
|
3664
|
-
process.stderr.write(
|
|
3665
|
-
"sync-approvals: could not resolve worktree id; proceeding without caller_worktree_id\n"
|
|
3666
|
-
);
|
|
3667
|
-
return void 0;
|
|
3668
|
-
} catch {
|
|
3669
|
-
return void 0;
|
|
3670
|
-
}
|
|
3671
|
-
}
|
|
3672
3643
|
var RETRY_DELAY_MS;
|
|
3673
3644
|
var init_round = __esm({
|
|
3674
3645
|
"src/cli/round.ts"() {
|
|
@@ -3676,8 +3647,6 @@ var init_round = __esm({
|
|
|
3676
3647
|
init_api();
|
|
3677
3648
|
init_mcp_client();
|
|
3678
3649
|
init_flags();
|
|
3679
|
-
init_resolve_worktree();
|
|
3680
|
-
init_local_config();
|
|
3681
3650
|
init_sync_approvals();
|
|
3682
3651
|
RETRY_DELAY_MS = 1e3;
|
|
3683
3652
|
}
|
|
@@ -4660,6 +4629,13 @@ async function runLocalMigration(projectPath) {
|
|
|
4660
4629
|
"utf-8"
|
|
4661
4630
|
);
|
|
4662
4631
|
filesChanged.push(".codebyplan/e2e.json");
|
|
4632
|
+
const eslintJson = {};
|
|
4633
|
+
await writeFile10(
|
|
4634
|
+
join14(projectPath, ".codebyplan", "eslint.json"),
|
|
4635
|
+
JSON.stringify(eslintJson, null, 2) + "\n",
|
|
4636
|
+
"utf-8"
|
|
4637
|
+
);
|
|
4638
|
+
filesChanged.push(".codebyplan/eslint.json");
|
|
4663
4639
|
if (!deviceWrittenByHelper) {
|
|
4664
4640
|
await writeFile10(
|
|
4665
4641
|
join14(projectPath, ".codebyplan", "device.local.json"),
|
|
@@ -4895,6 +4871,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
4895
4871
|
}
|
|
4896
4872
|
const vendorPayload = {};
|
|
4897
4873
|
const e2ePayload = {};
|
|
4874
|
+
const eslintPayload = {};
|
|
4898
4875
|
if (dryRun) {
|
|
4899
4876
|
console.log(" Config would be updated (dry-run).");
|
|
4900
4877
|
return;
|
|
@@ -4906,7 +4883,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
|
|
|
4906
4883
|
{ name: "git.json", payload: gitPayload },
|
|
4907
4884
|
{ name: "shipment.json", payload: shipmentPayload },
|
|
4908
4885
|
{ name: "vendor.json", payload: vendorPayload },
|
|
4909
|
-
{ name: "e2e.json", payload: e2ePayload, createOnly: true }
|
|
4886
|
+
{ name: "e2e.json", payload: e2ePayload, createOnly: true },
|
|
4887
|
+
{ name: "eslint.json", payload: eslintPayload, createOnly: true }
|
|
4910
4888
|
];
|
|
4911
4889
|
let anyUpdated = false;
|
|
4912
4890
|
for (const { name, payload, createOnly } of files) {
|
|
@@ -6788,8 +6766,9 @@ void (async () => {
|
|
|
6788
6766
|
}
|
|
6789
6767
|
if (arg === "login") {
|
|
6790
6768
|
const { runLogin: runLogin2 } = await Promise.resolve().then(() => (init_login(), login_exports));
|
|
6769
|
+
const admin = process.argv.includes("--admin");
|
|
6791
6770
|
try {
|
|
6792
|
-
await runLogin2();
|
|
6771
|
+
await runLogin2({ admin });
|
|
6793
6772
|
process.exit(0);
|
|
6794
6773
|
} catch {
|
|
6795
6774
|
process.exit(1);
|
|
@@ -6933,7 +6912,7 @@ void (async () => {
|
|
|
6933
6912
|
|
|
6934
6913
|
Usage:
|
|
6935
6914
|
codebyplan setup Interactive setup (OAuth + project init)
|
|
6936
|
-
codebyplan login
|
|
6915
|
+
codebyplan login [--admin] Authenticate via OAuth device-code flow
|
|
6937
6916
|
codebyplan logout Clear cached OAuth tokens
|
|
6938
6917
|
codebyplan whoami Show currently authenticated identity
|
|
6939
6918
|
codebyplan upgrade-auth Migrate legacy x-api-key MCP config to OAuth
|
package/package.json
CHANGED
|
@@ -228,18 +228,6 @@ Denies any `git stash` command (including `git -C <dir> stash` and `git stash po
|
|
|
228
228
|
|
|
229
229
|
---
|
|
230
230
|
|
|
231
|
-
### `cbp-mcp-worktree-inject.sh` — PreToolUse, matcher `mcp__codebyplan__(update_task|complete_task|complete_round|update_checkpoint)`
|
|
232
|
-
|
|
233
|
-
Auto-injects `caller_worktree_id` into CodeByPlan MCP mutation calls when it's missing, resolving it via `npx codebyplan resolve-worktree` (with `--fallback-from-branch`). Closes the manual worktree-pinning workaround so MCP writes pass the server-side worktree pre-guard without hand-editing every call.
|
|
234
|
-
|
|
235
|
-
**Blocks vs warns**: neither — emits an `updatedInput` to add the field when resolvable, otherwise passes the call through unchanged (graceful passthrough preserves backwards-compat).
|
|
236
|
-
|
|
237
|
-
**Skips when**: the call already carries `caller_worktree_id`, or no worktree can be resolved (both → clean passthrough). A no-op in repos that don't use the CodeByPlan MCP.
|
|
238
|
-
|
|
239
|
-
**Opt out**: settings.json override removing this entry, or plugin disable.
|
|
240
|
-
|
|
241
|
-
---
|
|
242
|
-
|
|
243
231
|
### `cbp-mcp-round-sync.sh` — PostToolUse, matcher `mcp__codebyplan__complete_round`
|
|
244
232
|
|
|
245
233
|
After a `complete_round` MCP call succeeds, reconciles the round's `files_changed[]` against `git status`: new files in the diff are added (unapproved), and approval records no longer in the diff are flagged `removed_from_diff` (never deleted — lifecycle preserved).
|
|
@@ -258,7 +246,7 @@ After a `complete_round` MCP call succeeds, reconciles the round's `files_change
|
|
|
258
246
|
|
|
259
247
|
Test suite for the plugin's 10 registered hooks. Runs two passes:
|
|
260
248
|
|
|
261
|
-
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-
|
|
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.
|
|
262
250
|
2. **Functional smoke tests** — each hook is invoked with synthetic stdin matching its fast-path / graceful-degrade input; all must exit 0.
|
|
263
251
|
|
|
264
252
|
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`.
|
|
@@ -64,6 +64,14 @@ while IFS= read -r FILE; do
|
|
|
64
64
|
continue
|
|
65
65
|
fi
|
|
66
66
|
|
|
67
|
+
# Skip files under a __tests__/ directory — fixtures, helpers, setup, and
|
|
68
|
+
# other test infrastructure are imported by the test files that exercise
|
|
69
|
+
# them; requiring a dedicated .test.ts for a fixture is nonsensical.
|
|
70
|
+
if echo "$FILE" | grep -qE '/__tests__/'; then
|
|
71
|
+
SKIPPED=$((SKIPPED + 1))
|
|
72
|
+
continue
|
|
73
|
+
fi
|
|
74
|
+
|
|
67
75
|
# Skip infrastructure / generated / config files (generic skips — harmless if user doesn't have these dirs)
|
|
68
76
|
if echo "$FILE" | grep -qE '^\.claude/|^docs/|^supabase/|\.config\.|\.json$|\.md$|\.ya?ml$|\.sh$|\.scss$|\.css$'; then
|
|
69
77
|
continue
|
|
@@ -207,48 +207,6 @@ else
|
|
|
207
207
|
fi
|
|
208
208
|
fi
|
|
209
209
|
|
|
210
|
-
# --- cbp-mcp-worktree-inject.sh ---
|
|
211
|
-
|
|
212
|
-
if [ ! -f "$HOOKS_DIR/cbp-mcp-worktree-inject.sh" ]; then
|
|
213
|
-
test_result "cbp-mcp-worktree-inject.sh present" "passed" "missing"
|
|
214
|
-
else
|
|
215
|
-
test_result "cbp-mcp-worktree-inject.sh present" "passed" "passed"
|
|
216
|
-
|
|
217
|
-
FIRST_LINE=$(head -1 "$HOOKS_DIR/cbp-mcp-worktree-inject.sh")
|
|
218
|
-
if echo "$FIRST_LINE" | grep -q '^#!/'; then
|
|
219
|
-
test_result "cbp-mcp-worktree-inject.sh has shebang" "passed" "passed"
|
|
220
|
-
else
|
|
221
|
-
test_result "cbp-mcp-worktree-inject.sh has shebang" "passed" "missing"
|
|
222
|
-
fi
|
|
223
|
-
|
|
224
|
-
if grep -q '@scope: org-shared' "$HOOKS_DIR/cbp-mcp-worktree-inject.sh"; then
|
|
225
|
-
test_result "cbp-mcp-worktree-inject.sh has @scope: org-shared" "passed" "passed"
|
|
226
|
-
else
|
|
227
|
-
test_result "cbp-mcp-worktree-inject.sh has @scope: org-shared" "passed" "missing"
|
|
228
|
-
fi
|
|
229
|
-
|
|
230
|
-
FIXTURES_DIR="$HOOKS_DIR/__test-fixtures__/cbp-mcp-worktree-inject"
|
|
231
|
-
if [ -d "$FIXTURES_DIR" ]; then
|
|
232
|
-
# already-has-id.json — expect exit 0 (passthrough)
|
|
233
|
-
ACTUAL_EXIT=$(bash "$HOOKS_DIR/cbp-mcp-worktree-inject.sh" < "$FIXTURES_DIR/already-has-id.json" >/dev/null 2>&1; echo $?)
|
|
234
|
-
if [ "$ACTUAL_EXIT" = "0" ]; then
|
|
235
|
-
test_result "cbp-mcp-worktree-inject.sh already-has-id exits 0" "passed" "passed"
|
|
236
|
-
else
|
|
237
|
-
test_result "cbp-mcp-worktree-inject.sh already-has-id exits 0" "passed" "failed (exit $ACTUAL_EXIT)"
|
|
238
|
-
fi
|
|
239
|
-
|
|
240
|
-
# missing-id-both-empty.json — resolvers return empty (in test env), expect exit 0
|
|
241
|
-
ACTUAL_EXIT=$(bash "$HOOKS_DIR/cbp-mcp-worktree-inject.sh" < "$FIXTURES_DIR/missing-id-both-empty.json" >/dev/null 2>&1; echo $?)
|
|
242
|
-
if [ "$ACTUAL_EXIT" = "0" ]; then
|
|
243
|
-
test_result "cbp-mcp-worktree-inject.sh missing-id passthrough exits 0" "passed" "passed"
|
|
244
|
-
else
|
|
245
|
-
test_result "cbp-mcp-worktree-inject.sh missing-id passthrough exits 0" "passed" "failed (exit $ACTUAL_EXIT)"
|
|
246
|
-
fi
|
|
247
|
-
else
|
|
248
|
-
test_result "cbp-mcp-worktree-inject.sh fixtures dir present" "passed" "missing"
|
|
249
|
-
fi
|
|
250
|
-
fi
|
|
251
|
-
|
|
252
210
|
# --- cbp-mcp-round-sync.sh ---
|
|
253
211
|
|
|
254
212
|
if [ ! -f "$HOOKS_DIR/cbp-mcp-round-sync.sh" ]; then
|
|
@@ -35,15 +35,6 @@
|
|
|
35
35
|
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-mcp-migration-guard.sh"
|
|
36
36
|
}
|
|
37
37
|
]
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"matcher": "mcp__codebyplan__(update_task|complete_task|complete_round|update_checkpoint)",
|
|
41
|
-
"hooks": [
|
|
42
|
-
{
|
|
43
|
-
"type": "command",
|
|
44
|
-
"command": "bash ${CLAUDE_PLUGIN_ROOT}/hooks/cbp-mcp-worktree-inject.sh"
|
|
45
|
-
}
|
|
46
|
-
]
|
|
47
38
|
}
|
|
48
39
|
],
|
|
49
40
|
"PostToolUse": [
|
|
@@ -57,7 +57,7 @@ This mirrors the CHK-104 hard-lock model — never wrest a checkpoint from a liv
|
|
|
57
57
|
|
|
58
58
|
If the checkpoint is already `active` AND `worktree_id` already equals `CALLER_WT` (the Step 3 no-op row), skip this step entirely and proceed to Step 5 — nothing to write.
|
|
59
59
|
|
|
60
|
-
Otherwise set the checkpoint `active` via MCP `update_checkpoint(checkpoint_id, status: "active"`, plus `worktree_id: CALLER_WT` when claiming per Step 3
|
|
60
|
+
Otherwise set the checkpoint `active` via MCP `update_checkpoint(checkpoint_id, status: "active"`, plus `worktree_id: CALLER_WT` when claiming per Step 3. The server resolves the caller's worktree identity from the JWT/ctx (CHK-140 TASK-3 — `caller_worktree_id` input field removed). If the checkpoint was already `active` but a claim is still needed, skip the status write and only write `worktree_id`.
|
|
61
61
|
|
|
62
62
|
### Step 5: Route
|
|
63
63
|
|
|
@@ -78,7 +78,7 @@ Show a one-line confirmation before routing:
|
|
|
78
78
|
## Integration
|
|
79
79
|
|
|
80
80
|
- **Reads**: MCP `get_checkpoints`, `get_tasks`; `npx codebyplan resolve-worktree`
|
|
81
|
-
- **Writes**: MCP `update_checkpoint` (status + worktree_id
|
|
81
|
+
- **Writes**: MCP `update_checkpoint` (status + worktree_id; server resolves caller worktree from JWT/ctx)
|
|
82
82
|
- **Triggered by**: `/cbp-checkpoint-plan` (auto when claimed at create), `/cbp-todo` (planned-but-pending gate), or user directly
|
|
83
83
|
- **Triggers**: `/cbp-task-start` (auto when claimed), or `/cbp-checkpoint-plan` (when the checkpoint is unplanned)
|
|
84
84
|
- **Never**: plans or creates tasks — that is `/cbp-checkpoint-plan`
|
|
@@ -49,7 +49,7 @@ RESOLVE_JSON=$(npx codebyplan resolve-worktree --json)
|
|
|
49
49
|
|
|
50
50
|
Extract `worktree_id` and `error_kind` from the JSON output.
|
|
51
51
|
|
|
52
|
-
- `error_kind` is `null` or `"tuple_miss"` → healthy. `WORKTREE_ID` = `worktree_id` (may be `null`: a legitimate main-repo or unregistered-worktree case — proceed normally;
|
|
52
|
+
- `error_kind` is `null` or `"tuple_miss"` → healthy. `WORKTREE_ID` = `worktree_id` (may be `null`: a legitimate main-repo or unregistered-worktree case — proceed normally; the server resolves worktree identity from the JWT/ctx, falling back to the repo main-worktree when no specific worktree is matched).
|
|
53
53
|
- `error_kind` is `local_config_read_failed`, `local_config_write_failed`, `legacy_file_blocks_dir`, `api_failed`, `git_failed`, or `unhandled` → **broken local state**. Hold the `error_kind` for Step 6 to display as a distress warning. Session continues (non-blocking — unlike `/cbp-todo`, session-start does NOT hard-stop on a non-tuple-miss distress).
|
|
54
54
|
|
|
55
55
|
Pass `WORKTREE_ID` to MCP tools that support it. Null `WORKTREE_ID` means the (device, path, branch) tuple is unregistered — note this for Step 6.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
scope: org-shared
|
|
3
|
-
name: cbp-e2e
|
|
3
|
+
name: cbp-setup-e2e
|
|
4
4
|
description: Detect installed E2E frameworks, ask which to enable, record credentials source (gitignored env-file path + var names only, never secrets), and write/refresh .codebyplan/e2e.json. Interactive, idempotent.
|
|
5
5
|
argument-hint: "[--force]"
|
|
6
6
|
model: sonnet
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
scope: org-shared
|
|
3
|
+
name: cbp-setup-eslint
|
|
4
|
+
description: Detect each app's tech stack, resolve matching DB ESLint presets, confirm which to enable per app, run `codebyplan eslint init` to generate eslint.config.mjs, and write/refresh .codebyplan/eslint.json. 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(ls *), Bash(mkdir *), Bash(cp *), Bash(echo *), Bash(mv *), Bash(npx codebyplan eslint *), AskUserQuestion, mcp__codebyplan__get_repos, mcp__codebyplan__get_eslint_presets
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# ESLint Setup
|
|
12
|
+
|
|
13
|
+
Configure ESLint flat config (`eslint.config.mjs`) across the repo's apps: detect each
|
|
14
|
+
app's tech stack, resolve the DB ESLint presets that match it, generate per-app configs via
|
|
15
|
+
`codebyplan eslint init`, and record the enabled presets in `.codebyplan/eslint.json` so the
|
|
16
|
+
setup is reproducible and refreshes idempotently.
|
|
17
|
+
|
|
18
|
+
Invoke at any time. Already-configured apps are preserved unless `--force` is passed.
|
|
19
|
+
Pass `--force` to re-ask every app and re-resolve presets from scratch.
|
|
20
|
+
|
|
21
|
+
The DB ESLint presets are the source of truth for rule bodies; `.codebyplan/eslint.json`
|
|
22
|
+
only records *which* presets are enabled per app (the rules live in the generated
|
|
23
|
+
`eslint.config.mjs` and the DB). See `reference/*.md` for the latest official flat-config
|
|
24
|
+
setup per stack — including stacks that have **no preset yet** (manual-config guidance).
|
|
25
|
+
|
|
26
|
+
## Arguments
|
|
27
|
+
|
|
28
|
+
Inspect `$ARGUMENTS` for `--force`. If present, set `force_mode = true`.
|
|
29
|
+
Absent: idempotent mode — preserve existing `apps[*]` entries in `.codebyplan/eslint.json`,
|
|
30
|
+
skip re-asking already-configured apps.
|
|
31
|
+
|
|
32
|
+
## Step 1 — Detect apps + per-app tech
|
|
33
|
+
|
|
34
|
+
Run two detection signals and merge:
|
|
35
|
+
|
|
36
|
+
**Signal A — DB tech_stack** via `mcp__codebyplan__get_repos` (match `repo_id` from
|
|
37
|
+
`.codebyplan/repo.json`). Read the repo's `tech_stack`:
|
|
38
|
+
- `tech_stack.apps[]` — each `{ name, path, stack[] }` is one app/package (monorepo).
|
|
39
|
+
- `tech_stack.flat[]` — the deduped repo-wide stack (single-app repos, or when `apps[]` is empty).
|
|
40
|
+
|
|
41
|
+
**Signal B — Filesystem fallback** (when `apps[]` is empty or stale). Discover workspaces the
|
|
42
|
+
same way `tech-detect.ts` does, then probe each app's `package.json`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# workspace globs → app dirs that contain a package.json
|
|
46
|
+
cat pnpm-workspace.yaml 2>/dev/null # packages: [...] globs
|
|
47
|
+
jq -r '.workspaces[]?' package.json 2>/dev/null # or package.json#workspaces
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
For each app dir, read `package.json` dependencies to infer the stack (next → Next.js,
|
|
51
|
+
react → React, @nestjs/* → NestJS, vitest → Vitest, jest → Jest, @playwright/test →
|
|
52
|
+
Playwright, tailwindcss → Tailwind, expo → Expo, etc.). A single-app repo is one target
|
|
53
|
+
with `source_path: "."` and `app: "root"`.
|
|
54
|
+
|
|
55
|
+
**Signal C — Read existing `.codebyplan/eslint.json`** for the idempotent merge:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cat .codebyplan/eslint.json 2>/dev/null || echo '{}'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
An app already present under `apps[<source_path>]` is "configured"; in non-`--force` mode it
|
|
62
|
+
is skipped in Step 3 (presets preserved verbatim).
|
|
63
|
+
|
|
64
|
+
## Step 2 — Resolve matched presets
|
|
65
|
+
|
|
66
|
+
Call `mcp__codebyplan__get_eslint_presets` with `repo_id`. The response gives:
|
|
67
|
+
- `presets[]` — all system presets (base, nextjs, react, node, cli, testing, testing-react, testing-e2e).
|
|
68
|
+
- `matched[]` — the subset whose `tech_match` (requires / excludes / requires_capabilities) the
|
|
69
|
+
repo's DB tech_stack satisfies. **Use this — do not re-implement the matching client-side.**
|
|
70
|
+
|
|
71
|
+
`matched[]` is repo-wide. Attribute presets to apps by intersecting each app's detected tech
|
|
72
|
+
with each preset's `tech_match.requires` (e.g. `nextjs` → apps with Next.js; `node`/`cli` →
|
|
73
|
+
the Node/CLI packages; `testing-e2e` → apps with Playwright). The `base` preset applies to
|
|
74
|
+
every TypeScript app.
|
|
75
|
+
|
|
76
|
+
**Gap stacks have NO preset.** NestJS, React Native/Expo, Jest, Tailwind, and
|
|
77
|
+
WebdriverIO/Mocha are real stacks with no system preset. When an app's tech includes one,
|
|
78
|
+
flag it in Step 3 and point at its `reference/*.md` for manual setup — never silently drop it.
|
|
79
|
+
|
|
80
|
+
## Step 3 — Confirm presets per app
|
|
81
|
+
|
|
82
|
+
For each detected app NOT already configured (or every app in `--force` mode), present its
|
|
83
|
+
matched presets and ask via AskUserQuestion (one batch per app, matched pre-checked):
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
App: apps/web (Next.js + React + SCSS + Playwright + Vitest)
|
|
87
|
+
Enable which ESLint presets?
|
|
88
|
+
[x] base (TypeScript + security + Prettier)
|
|
89
|
+
[x] nextjs (Core Web Vitals + jsx-a11y + import order)
|
|
90
|
+
[x] testing (Vitest)
|
|
91
|
+
[x] testing-react (Testing Library + jest-dom)
|
|
92
|
+
[x] testing-e2e (Playwright)
|
|
93
|
+
Gap stacks with no preset (manual setup — see reference/):
|
|
94
|
+
· tailwind → reference/tailwind.md (if this app uses Tailwind)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Record the confirmed preset-name list per app. Toggling is allowed; an app may opt out of a
|
|
98
|
+
matched preset or in to an unmatched one.
|
|
99
|
+
|
|
100
|
+
## Step 4 — Generate config (`codebyplan eslint init`)
|
|
101
|
+
|
|
102
|
+
Offer to run the CLI, which detects tech, resolves presets, generates each
|
|
103
|
+
`eslint.config.mjs`, installs missing deps, and saves the DB config:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx codebyplan eslint init
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Degrade gracefully.** The CLI is sometimes unavailable (OAuth/MCP env, stale bin). If it
|
|
110
|
+
errors or is absent, do NOT hard-fail — print the command for the user to run manually and
|
|
111
|
+
continue to Step 5 (the `.codebyplan/eslint.json` record is written regardless). For gap
|
|
112
|
+
stacks, point the user at the matching `reference/*.md` to hand-author the config.
|
|
113
|
+
|
|
114
|
+
## Step 5 — Write `.codebyplan/eslint.json`
|
|
115
|
+
|
|
116
|
+
Build the payload conforming to the `EslintLocalConfig` schema
|
|
117
|
+
(`packages/codebyplan-package/src/lib/types.ts`): an `apps` map keyed by `source_path`, each
|
|
118
|
+
value an `EslintAppConfig` `{ app, enabled_presets[], rule_overrides?, config_path, reference_docs? }`.
|
|
119
|
+
|
|
120
|
+
Idempotency rule: deep-merge the new per-app entries onto the existing `apps` map so apps
|
|
121
|
+
skipped by the Step 3 gate keep their prior entry. Assemble in the shell, then write
|
|
122
|
+
atomically (tmp + mv) so the file is never left partial:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
EXISTING_APPS=$(jq -c '.apps // {}' .codebyplan/eslint.json 2>/dev/null || echo '{}')
|
|
126
|
+
APPS_JSON=$(echo "$EXISTING_APPS" | jq -c --argjson new "$NEW_APPS_JSON" '. * $new')
|
|
127
|
+
jq -n --argjson apps "$APPS_JSON" '{apps: $apps}' \
|
|
128
|
+
> .codebyplan/eslint.json.tmp && mv .codebyplan/eslint.json.tmp .codebyplan/eslint.json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Example populated entry:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"apps": {
|
|
136
|
+
"apps/web": {
|
|
137
|
+
"app": "web",
|
|
138
|
+
"enabled_presets": ["base", "nextjs", "testing", "testing-react", "testing-e2e"],
|
|
139
|
+
"config_path": "apps/web/eslint.config.mjs",
|
|
140
|
+
"reference_docs": ["base", "nextjs", "testing-react", "e2e"]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Only the `apps` field is written — `EslintLocalConfig` defines no other. Never store rule
|
|
147
|
+
bodies here; they live in the DB presets and the generated `eslint.config.mjs`.
|
|
148
|
+
|
|
149
|
+
## Step 6 — Verify and report
|
|
150
|
+
|
|
151
|
+
Re-read `.codebyplan/eslint.json` and emit a per-app summary:
|
|
152
|
+
|
|
153
|
+
```
|
|
154
|
+
ESLint Setup — Complete
|
|
155
|
+
|
|
156
|
+
App | Presets | Config | Gaps
|
|
157
|
+
---------------------------- | ------------------------------------ | ------------------- | --------
|
|
158
|
+
apps/web | base, nextjs, testing, testing-react | apps/web/eslint…mjs | —
|
|
159
|
+
packages/codebyplan-package | base, node, cli, testing | …/eslint.config.mjs | —
|
|
160
|
+
|
|
161
|
+
eslint.json written to .codebyplan/eslint.json
|
|
162
|
+
|
|
163
|
+
Per-stack setup references — see reference docs:
|
|
164
|
+
base/typescript → reference/base.md nextjs → reference/nextjs.md
|
|
165
|
+
react → reference/react.md node → reference/node.md
|
|
166
|
+
nestjs → reference/nestjs.md cli → reference/cli.md
|
|
167
|
+
tailwind → reference/tailwind.md expo → reference/react-native.md
|
|
168
|
+
vitest → reference/vitest.md jest → reference/jest.md
|
|
169
|
+
testing-react → reference/testing-react.md e2e → reference/e2e.md
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
If `codebyplan eslint init` was skipped (CLI unavailable), say so and restate the manual
|
|
173
|
+
command + which apps still need their `eslint.config.mjs` generated.
|
|
174
|
+
|
|
175
|
+
## Key Rules
|
|
176
|
+
|
|
177
|
+
- DB presets are the source of truth for rule bodies — `.codebyplan/eslint.json` records only
|
|
178
|
+
which presets are enabled per app
|
|
179
|
+
- `get_eslint_presets` `matched[]` drives preset resolution — never re-implement tech matching
|
|
180
|
+
- Gap stacks (NestJS, Expo, Jest, Tailwind, WebdriverIO/Mocha) have no preset — point at
|
|
181
|
+
`reference/*.md`, never silently skip
|
|
182
|
+
- `codebyplan eslint init` failure is non-fatal — print the manual command and still write eslint.json
|
|
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)
|
|
186
|
+
|
|
187
|
+
## Additional resources
|
|
188
|
+
|
|
189
|
+
- 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
|
+
- Node / Hono / Express: [reference/node.md](reference/node.md) · NestJS: [reference/nestjs.md](reference/nestjs.md)
|
|
192
|
+
- CLI tools: [reference/cli.md](reference/cli.md) · Tailwind CSS: [reference/tailwind.md](reference/tailwind.md)
|
|
193
|
+
- React Native / Expo: [reference/react-native.md](reference/react-native.md)
|
|
194
|
+
- Vitest: [reference/vitest.md](reference/vitest.md) · Jest: [reference/jest.md](reference/jest.md)
|
|
195
|
+
- Testing Library + jest-dom: [reference/testing-react.md](reference/testing-react.md)
|
|
196
|
+
- E2E (Playwright / WebdriverIO / Mocha): [reference/e2e.md](reference/e2e.md)
|
|
197
|
+
- ESLint local-config schema: `packages/codebyplan-package/src/lib/types.ts` (`EslintLocalConfig`)
|
|
198
|
+
- DB presets + generator: `mcp__codebyplan__get_eslint_presets`, `codebyplan eslint init`
|
|
199
|
+
- VS Code extension lint is not yet covered by a reference doc — use `reference/base.md` + `reference/node.md`.
|