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.
Files changed (30) hide show
  1. package/dist/cli.js +50 -71
  2. package/package.json +1 -1
  3. package/templates/hooks/README.md +1 -13
  4. package/templates/hooks/cbp-test-coverage-gate.sh +8 -0
  5. package/templates/hooks/cbp-test-hooks.sh +0 -42
  6. package/templates/hooks/hooks.json +0 -9
  7. package/templates/skills/cbp-checkpoint-start/SKILL.md +2 -2
  8. package/templates/skills/cbp-session-start/SKILL.md +1 -1
  9. package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/SKILL.md +1 -1
  10. package/templates/skills/cbp-setup-eslint/SKILL.md +199 -0
  11. package/templates/skills/cbp-setup-eslint/reference/base.md +82 -0
  12. package/templates/skills/cbp-setup-eslint/reference/cli.md +56 -0
  13. package/templates/skills/cbp-setup-eslint/reference/e2e.md +68 -0
  14. package/templates/skills/cbp-setup-eslint/reference/jest.md +59 -0
  15. package/templates/skills/cbp-setup-eslint/reference/nestjs.md +69 -0
  16. package/templates/skills/cbp-setup-eslint/reference/nextjs.md +63 -0
  17. package/templates/skills/cbp-setup-eslint/reference/node.md +74 -0
  18. package/templates/skills/cbp-setup-eslint/reference/react-native.md +60 -0
  19. package/templates/skills/cbp-setup-eslint/reference/react.md +82 -0
  20. package/templates/skills/cbp-setup-eslint/reference/tailwind.md +64 -0
  21. package/templates/skills/cbp-setup-eslint/reference/testing-react.md +57 -0
  22. package/templates/skills/cbp-setup-eslint/reference/vitest.md +62 -0
  23. package/templates/skills/cbp-task-complete/SKILL.md +1 -3
  24. package/templates/skills/cbp-task-start/SKILL.md +3 -3
  25. package/templates/hooks/cbp-mcp-worktree-inject.sh +0 -76
  26. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/maestro.md +0 -0
  27. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/playwright.md +0 -0
  28. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/tauri.md +0 -0
  29. /package/templates/skills/{cbp-e2e-setup → cbp-setup-e2e}/reference/vscode.md +0 -0
  30. /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.12.0";
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({ client_id: clientId }),
989
+ body: JSON.stringify(body),
986
990
  signal: AbortSignal.timeout(1e4)
987
991
  });
988
992
  if (!res.ok) {
989
- const body = await res.json().catch(() => ({}));
990
- if (body.error === "invalid_client") {
993
+ const body2 = await res.json().catch(() => ({}));
994
+ if (body2.error === "invalid_client") {
991
995
  throw new OAuthInvalidClientError();
992
996
  }
993
- const msg = body.error_description ?? body.error ?? `HTTP ${res.status}`;
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
- return await res.json();
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(opts.deviceCode, opts.clientId);
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 --worktree-id <uuid> Worktree UUID (optional; auto-resolved if absent)\n --dry-run Print merged payload to stdout without writing\n\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
- const roundArgs = {
3601
+ await mcpCall("update_round", {
3587
3602
  round_id: roundId,
3588
3603
  files_changed: result.merged_files_changed
3589
- };
3590
- if (callerWorktreeId) {
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 Authenticate via OAuth device-code flow
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebyplan",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "description": "CLI for CodeByPlan — AI-powered development planning and tracking",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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-worktree-inject`, `cbp-mcp-round-sync`) carries the required `# Hook:` and `# Purpose:` header comments. `statusline` uses its own `# Claude Code Status Line` marker.
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, plus `caller_worktree_id: CALLER_WT` so the hard-lock pre-guard accepts the call (omit `caller_worktree_id` only when `CALLER_WT` is empty). If the checkpoint was already `active` but a claim is still needed, skip the status write and only write `worktree_id`.
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, with caller_worktree_id pre-guard)
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; downstream hard-lock pre-guards may reject mutations on assigned rows).
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-setup
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`.