codebyplan 1.11.1 → 1.12.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 (56) hide show
  1. package/dist/cli.js +602 -345
  2. package/package.json +1 -1
  3. package/templates/README.md +1 -1
  4. package/templates/agents/cbp-cc-executor.md +1 -1
  5. package/templates/agents/cbp-e2e-maestro.md +202 -0
  6. package/templates/agents/cbp-e2e-playwright.md +229 -0
  7. package/templates/agents/cbp-e2e-tauri.md +184 -0
  8. package/templates/agents/cbp-e2e-vscode.md +203 -0
  9. package/templates/agents/cbp-e2e-xcuitest.md +224 -0
  10. package/templates/agents/cbp-improve-claude.md +1 -1
  11. package/templates/agents/cbp-round-executor.md +11 -11
  12. package/templates/agents/cbp-task-check.md +1 -1
  13. package/templates/agents/cbp-task-planner.md +2 -0
  14. package/templates/agents/cbp-testing-qa-agent.md +9 -9
  15. package/templates/context/testing/e2e.md +303 -0
  16. package/templates/hooks/cbp-statusline.mjs +44 -0
  17. package/templates/hooks/cbp-statusline.py +24 -2
  18. package/templates/hooks/cbp-statusline.sh +22 -2
  19. package/templates/hooks/validate-structure-lengths.sh +2 -0
  20. package/templates/hooks/validate-structure-smoke.sh +2 -1
  21. package/templates/hooks/validate-structure-templates.sh +1 -0
  22. package/templates/rules/README.md +8 -1
  23. package/templates/rules/context-file-loading.md +4 -1
  24. package/templates/rules/e2e-mandatory.md +70 -0
  25. package/templates/rules/supabase-branch-lifecycle.md +99 -0
  26. package/templates/settings.project.base.json +1 -2
  27. package/templates/skills/cbp-build-cc-agent/SKILL.md +16 -14
  28. package/templates/skills/cbp-build-cc-agent/reference/cbp-quality.md +4 -4
  29. package/templates/skills/cbp-build-cc-agent/scripts/validate-agent.sh +8 -6
  30. package/templates/skills/cbp-build-cc-mode/SKILL.md +4 -4
  31. package/templates/skills/cbp-build-cc-settings/reference/cbp-conventions.md +1 -2
  32. package/templates/skills/cbp-checkpoint-check/SKILL.md +12 -8
  33. package/templates/skills/cbp-checkpoint-create/SKILL.md +2 -0
  34. package/templates/skills/cbp-checkpoint-end/SKILL.md +27 -5
  35. package/templates/skills/cbp-checkpoint-plan/SKILL.md +2 -2
  36. package/templates/skills/cbp-checkpoint-plan/reference/e2e-discovery-probe.md +5 -5
  37. package/templates/skills/cbp-e2e-setup/SKILL.md +254 -0
  38. package/templates/skills/cbp-e2e-setup/reference/maestro.md +200 -0
  39. package/templates/skills/cbp-e2e-setup/reference/playwright.md +212 -0
  40. package/templates/skills/cbp-e2e-setup/reference/tauri.md +147 -0
  41. package/templates/skills/cbp-e2e-setup/reference/vscode.md +154 -0
  42. package/templates/skills/cbp-e2e-setup/reference/xcuitest.md +185 -0
  43. package/templates/skills/cbp-frontend-ui/SKILL.md +6 -6
  44. package/templates/skills/cbp-frontend-ux/SKILL.md +1 -1
  45. package/templates/skills/cbp-git-worktree-remove/SKILL.md +17 -1
  46. package/templates/skills/cbp-round-execute/SKILL.md +30 -17
  47. package/templates/skills/cbp-session-start/SKILL.md +27 -2
  48. package/templates/skills/cbp-ship-main/SKILL.md +13 -0
  49. package/templates/skills/cbp-supabase-branch-check/SKILL.md +12 -5
  50. package/templates/skills/cbp-supabase-migrate/SKILL.md +139 -9
  51. package/templates/skills/cbp-supabase-migrate/reference/preflight-dry-run.md +1 -1
  52. package/templates/skills/cbp-supabase-setup/SKILL.md +13 -7
  53. package/templates/skills/cbp-supabase-setup/reference/branching-setup.md +2 -2
  54. package/templates/skills/cbp-task-check/SKILL.md +2 -2
  55. package/templates/skills/cbp-task-start/SKILL.md +2 -0
  56. package/templates/agents/cbp-test-e2e-agent.md +0 -363
package/dist/cli.js CHANGED
@@ -14,26 +14,163 @@ var VERSION, PACKAGE_NAME;
14
14
  var init_version = __esm({
15
15
  "src/lib/version.ts"() {
16
16
  "use strict";
17
- VERSION = "1.11.1";
17
+ VERSION = "1.12.0";
18
18
  PACKAGE_NAME = "codebyplan";
19
19
  }
20
20
  });
21
21
 
22
+ // src/lib/gitignore-block.ts
23
+ import { readFile, writeFile } from "node:fs/promises";
24
+ import * as path from "node:path";
25
+ async function ensureManagedGitignoreBlock(projectDir, dryRun = false) {
26
+ const gitignorePath = path.join(projectDir, ".gitignore");
27
+ let existing = null;
28
+ try {
29
+ existing = await readFile(gitignorePath, "utf-8");
30
+ } catch {
31
+ }
32
+ if (existing === null) {
33
+ if (!dryRun) {
34
+ const block = buildBlock("\n");
35
+ await writeFile(gitignorePath, block, "utf-8");
36
+ }
37
+ return "created";
38
+ }
39
+ const nl = existing.includes("\r\n") ? "\r\n" : "\n";
40
+ const lines = existing.split(/\r?\n/);
41
+ const startIdx = lines.findIndex((l) => l === GITIGNORE_BLOCK_START);
42
+ const endIdx = lines.findIndex((l) => l === GITIGNORE_BLOCK_END);
43
+ const hasValidBlock = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx;
44
+ if (!hasValidBlock) {
45
+ const hasOrphanMarker = startIdx !== -1 || endIdx !== -1;
46
+ if (hasOrphanMarker) {
47
+ const hadTrailingNewline = existing.endsWith("\n");
48
+ const cleanedLines = lines.filter(
49
+ (l) => l !== GITIGNORE_BLOCK_START && l !== GITIGNORE_BLOCK_END
50
+ );
51
+ if (hadTrailingNewline && cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1] === "") {
52
+ cleanedLines.pop();
53
+ }
54
+ let content2 = cleanedLines.join(nl);
55
+ if (content2.length > 0) content2 += nl;
56
+ const newContent3 = content2 + buildBlock(nl);
57
+ if (!dryRun) {
58
+ await writeFile(gitignorePath, newContent3, "utf-8");
59
+ }
60
+ return "refreshed";
61
+ }
62
+ let content = existing;
63
+ if (!content.endsWith("\n") && !content.endsWith("\r\n")) {
64
+ content = content + nl;
65
+ }
66
+ const block = buildBlock(nl);
67
+ const newContent2 = content + block;
68
+ if (!dryRun) {
69
+ await writeFile(gitignorePath, newContent2, "utf-8");
70
+ }
71
+ return "appended";
72
+ }
73
+ const blockBodyLines = lines.slice(startIdx + 1, endIdx);
74
+ const canonicalBodyLines = [...CANONICAL_GITIGNORE_ENTRIES];
75
+ const bodyMatches = blockBodyLines.length === canonicalBodyLines.length && blockBodyLines.every((l, i) => l === canonicalBodyLines[i]);
76
+ if (bodyMatches) {
77
+ return "unchanged";
78
+ }
79
+ const blockLines = [
80
+ GITIGNORE_BLOCK_START,
81
+ ...CANONICAL_GITIGNORE_ENTRIES,
82
+ GITIGNORE_BLOCK_END
83
+ ];
84
+ const newLines = [
85
+ ...lines.slice(0, startIdx),
86
+ ...blockLines,
87
+ ...lines.slice(endIdx + 1)
88
+ ];
89
+ const newContent = newLines.join(nl);
90
+ const finalContent = newContent.endsWith(nl) ? newContent : newContent + nl;
91
+ if (!dryRun) {
92
+ await writeFile(gitignorePath, finalContent, "utf-8");
93
+ }
94
+ return "refreshed";
95
+ }
96
+ async function removeManagedGitignoreBlock(projectDir, dryRun = false) {
97
+ const gitignorePath = path.join(projectDir, ".gitignore");
98
+ let existing = null;
99
+ try {
100
+ existing = await readFile(gitignorePath, "utf-8");
101
+ } catch {
102
+ return "unchanged";
103
+ }
104
+ const nl = existing.includes("\r\n") ? "\r\n" : "\n";
105
+ const hadTrailingNewline = existing.endsWith("\n");
106
+ const lines = existing.split(/\r?\n/);
107
+ if (hadTrailingNewline && lines.length > 0 && lines[lines.length - 1] === "") {
108
+ lines.pop();
109
+ }
110
+ const startIdx = lines.findIndex((l) => l === GITIGNORE_BLOCK_START);
111
+ const endIdx = lines.findIndex((l) => l === GITIGNORE_BLOCK_END);
112
+ const hasValidBlock = startIdx !== -1 && endIdx !== -1 && endIdx > startIdx;
113
+ if (!hasValidBlock) {
114
+ const hasOrphanMarker = startIdx !== -1 || endIdx !== -1;
115
+ if (!hasOrphanMarker) {
116
+ return "unchanged";
117
+ }
118
+ const cleaned = lines.filter(
119
+ (l) => l !== GITIGNORE_BLOCK_START && l !== GITIGNORE_BLOCK_END
120
+ );
121
+ const orphanContent = cleaned.length > 0 ? cleaned.join(nl) + nl : "";
122
+ if (!dryRun) {
123
+ await writeFile(gitignorePath, orphanContent, "utf-8");
124
+ }
125
+ return "removed";
126
+ }
127
+ const before = lines.slice(0, startIdx);
128
+ const after = lines.slice(endIdx + 1);
129
+ if (before.length > 0 && after.length > 0 && before[before.length - 1].trim() === "" && after[0].trim() === "") {
130
+ after.shift();
131
+ }
132
+ const merged = [...before, ...after];
133
+ const newContent = merged.length > 0 ? merged.join(nl) + nl : "";
134
+ if (!dryRun) {
135
+ await writeFile(gitignorePath, newContent, "utf-8");
136
+ }
137
+ return "removed";
138
+ }
139
+ function buildBlock(nl) {
140
+ return GITIGNORE_BLOCK_START + nl + CANONICAL_GITIGNORE_ENTRIES.join(nl) + nl + GITIGNORE_BLOCK_END + nl;
141
+ }
142
+ var CANONICAL_GITIGNORE_ENTRIES, GITIGNORE_BLOCK_START, GITIGNORE_BLOCK_END;
143
+ var init_gitignore_block = __esm({
144
+ "src/lib/gitignore-block.ts"() {
145
+ "use strict";
146
+ CANONICAL_GITIGNORE_ENTRIES = [
147
+ ".claude/settings.local.json",
148
+ ".claude/scheduled_tasks.lock",
149
+ ".codebyplan/device.local.json",
150
+ ".codebyplan/statusline.local.json",
151
+ ".codebyplan.local.json",
152
+ ".mcp.json"
153
+ ];
154
+ GITIGNORE_BLOCK_START = "# >>> codebyplan (managed) >>>";
155
+ GITIGNORE_BLOCK_END = "# <<< codebyplan <<<";
156
+ }
157
+ });
158
+
22
159
  // src/lib/local-config.ts
23
160
  import { execSync } from "node:child_process";
24
161
  import { createHash } from "node:crypto";
25
- import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
162
+ import { mkdir, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
26
163
  import { hostname } from "node:os";
27
- import { dirname, join } from "node:path";
164
+ import { dirname, join as join2 } from "node:path";
28
165
  function localConfigPath(projectPath) {
29
- return join(projectPath, ".codebyplan", "device.local.json");
166
+ return join2(projectPath, ".codebyplan", "device.local.json");
30
167
  }
31
168
  function legacyLocalConfigPath(projectPath) {
32
- return join(projectPath, ".codebyplan.local.json");
169
+ return join2(projectPath, ".codebyplan.local.json");
33
170
  }
34
171
  async function readLocalConfig(projectPath, onMigrationNotice) {
35
172
  try {
36
- const raw = await readFile(localConfigPath(projectPath), "utf-8");
173
+ const raw = await readFile2(localConfigPath(projectPath), "utf-8");
37
174
  const parsed = JSON.parse(raw);
38
175
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
39
176
  return parsed;
@@ -68,7 +205,7 @@ async function readLocalConfig(projectPath, onMigrationNotice) {
68
205
  }
69
206
  }
70
207
  try {
71
- const raw = await readFile(legacyLocalConfigPath(projectPath), "utf-8");
208
+ const raw = await readFile2(legacyLocalConfigPath(projectPath), "utf-8");
72
209
  const parsed = JSON.parse(raw);
73
210
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.device_id === "string") {
74
211
  onMigrationNotice?.(
@@ -88,8 +225,8 @@ async function readLocalConfig(projectPath, onMigrationNotice) {
88
225
  }
89
226
  async function writeLocalConfig(projectPath, config) {
90
227
  const content = { device_id: config.device_id };
91
- const path6 = localConfigPath(projectPath);
92
- const dirPath = dirname(path6);
228
+ const path7 = localConfigPath(projectPath);
229
+ const dirPath = dirname(path7);
93
230
  let phase = "stat config directory";
94
231
  try {
95
232
  try {
@@ -109,7 +246,7 @@ async function writeLocalConfig(projectPath, config) {
109
246
  phase = "create config directory";
110
247
  await mkdir(dirPath, { recursive: true });
111
248
  phase = "write local config";
112
- await writeFile(path6, JSON.stringify(content, null, 2) + "\n", "utf-8");
249
+ await writeFile2(path7, JSON.stringify(content, null, 2) + "\n", "utf-8");
113
250
  } catch (err) {
114
251
  const code = err.code;
115
252
  if (code === "LEGACY_FILE_BLOCKS_DIR") {
@@ -121,7 +258,7 @@ async function writeLocalConfig(projectPath, config) {
121
258
  }
122
259
  async function resolveMachineSeed() {
123
260
  try {
124
- const raw = await readFile("/etc/machine-id", "utf-8");
261
+ const raw = await readFile2("/etc/machine-id", "utf-8");
125
262
  const trimmed = raw.trim();
126
263
  if (trimmed) return trimmed;
127
264
  } catch {
@@ -153,13 +290,13 @@ var init_local_config = __esm({
153
290
 
154
291
  // src/lib/statusline-config.ts
155
292
  import { spawnSync } from "node:child_process";
156
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
157
- import { join as join2 } from "node:path";
293
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
294
+ import { join as join3 } from "node:path";
158
295
  function statuslineConfigPath(projectPath) {
159
- return join2(projectPath, ".codebyplan", "statusline.json");
296
+ return join3(projectPath, ".codebyplan", "statusline.json");
160
297
  }
161
298
  function statuslineLocalConfigPath(projectPath) {
162
- return join2(projectPath, ".codebyplan", "statusline.local.json");
299
+ return join3(projectPath, ".codebyplan", "statusline.local.json");
163
300
  }
164
301
  function isValidRenderer(value) {
165
302
  return typeof value === "string" && VALID_RENDERERS.has(value);
@@ -188,7 +325,7 @@ function mergeOverDefaults(raw) {
188
325
  }
189
326
  async function readStatuslineConfig(projectPath) {
190
327
  try {
191
- const raw = await readFile2(statuslineConfigPath(projectPath), "utf-8");
328
+ const raw = await readFile3(statuslineConfigPath(projectPath), "utf-8");
192
329
  const parsed = JSON.parse(raw);
193
330
  return mergeOverDefaults(parsed);
194
331
  } catch {
@@ -197,7 +334,7 @@ async function readStatuslineConfig(projectPath) {
197
334
  }
198
335
  async function readStatuslineLocalConfig(projectPath) {
199
336
  try {
200
- const raw = await readFile2(statuslineLocalConfigPath(projectPath), "utf-8");
337
+ const raw = await readFile3(statuslineLocalConfigPath(projectPath), "utf-8");
201
338
  const parsed = JSON.parse(raw);
202
339
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
203
340
  const obj = parsed;
@@ -217,8 +354,8 @@ async function writeStatuslineLocalConfig(projectPath, localConfig) {
217
354
  );
218
355
  }
219
356
  const filePath = statuslineLocalConfigPath(projectPath);
220
- await mkdir2(join2(projectPath, ".codebyplan"), { recursive: true });
221
- await writeFile2(
357
+ await mkdir2(join3(projectPath, ".codebyplan"), { recursive: true });
358
+ await writeFile3(
222
359
  filePath,
223
360
  JSON.stringify(localConfig, null, 2) + "\n",
224
361
  "utf-8"
@@ -258,7 +395,8 @@ var init_statusline_config = __esm({
258
395
  cost: true,
259
396
  rate_limits: true,
260
397
  repo_pr: true,
261
- worktree: true
398
+ worktree: true,
399
+ infra_drift: true
262
400
  },
263
401
  no_color: false
264
402
  };
@@ -297,9 +435,9 @@ var init_jwt_decode = __esm({
297
435
  });
298
436
 
299
437
  // src/oauth/keychain.ts
300
- import { chmod, mkdir as mkdir3, readFile as readFile3, unlink, writeFile as writeFile3 } from "node:fs/promises";
438
+ import { chmod, mkdir as mkdir3, readFile as readFile4, unlink, writeFile as writeFile4 } from "node:fs/promises";
301
439
  import { homedir, platform } from "node:os";
302
- import { dirname as dirname2, join as join3 } from "node:path";
440
+ import { dirname as dirname2, join as join4 } from "node:path";
303
441
  async function loadKeyring() {
304
442
  if (keyringOverride !== void 0) return keyringOverride;
305
443
  try {
@@ -311,30 +449,30 @@ async function loadKeyring() {
311
449
  }
312
450
  function fallbackDir() {
313
451
  if (platform() === "win32") {
314
- const appData = process.env.APPDATA ?? join3(homedir(), "AppData", "Roaming");
315
- return join3(appData, "codebyplan");
452
+ const appData = process.env.APPDATA ?? join4(homedir(), "AppData", "Roaming");
453
+ return join4(appData, "codebyplan");
316
454
  }
317
- const xdg = process.env.XDG_CONFIG_HOME ?? join3(homedir(), ".config");
318
- return join3(xdg, "codebyplan");
455
+ const xdg = process.env.XDG_CONFIG_HOME ?? join4(homedir(), ".config");
456
+ return join4(xdg, "codebyplan");
319
457
  }
320
458
  function fallbackFile(filename) {
321
- return join3(fallbackDir(), filename);
459
+ return join4(fallbackDir(), filename);
322
460
  }
323
461
  async function readFallback(filename) {
324
462
  try {
325
- const raw = await readFile3(fallbackFile(filename), "utf-8");
463
+ const raw = await readFile4(fallbackFile(filename), "utf-8");
326
464
  return JSON.parse(raw);
327
465
  } catch {
328
466
  return null;
329
467
  }
330
468
  }
331
469
  async function writeFallback(filename, data) {
332
- const path6 = fallbackFile(filename);
333
- await mkdir3(dirname2(path6), { recursive: true });
334
- await writeFile3(path6, JSON.stringify(data, null, 2) + "\n", "utf-8");
470
+ const path7 = fallbackFile(filename);
471
+ await mkdir3(dirname2(path7), { recursive: true });
472
+ await writeFile4(path7, JSON.stringify(data, null, 2) + "\n", "utf-8");
335
473
  if (platform() !== "win32") {
336
474
  try {
337
- await chmod(path6, 384);
475
+ await chmod(path7, 384);
338
476
  } catch {
339
477
  }
340
478
  }
@@ -541,8 +679,8 @@ async function getAuthHeaders() {
541
679
  return { headers: { "x-api-key": key }, via: "api_key" };
542
680
  }
543
681
  }
544
- function buildUrl(path6, params) {
545
- const url = new URL(`${baseUrl()}/api${path6}`);
682
+ function buildUrl(path7, params) {
683
+ const url = new URL(`${baseUrl()}/api${path7}`);
546
684
  if (params) {
547
685
  for (const [key, value] of Object.entries(params)) {
548
686
  if (value !== void 0) {
@@ -561,8 +699,8 @@ function isRetryable(err) {
561
699
  function delay(ms) {
562
700
  return new Promise((resolve5) => setTimeout(resolve5, ms));
563
701
  }
564
- async function request(method, path6, options) {
565
- const url = buildUrl(path6, options?.params);
702
+ async function request(method, path7, options) {
703
+ const url = buildUrl(path7, options?.params);
566
704
  const auth = await getAuthHeaders();
567
705
  let lastError;
568
706
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
@@ -582,7 +720,7 @@ async function request(method, path6, options) {
582
720
  signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
583
721
  });
584
722
  if (!res.ok) {
585
- let message = `API ${method} ${path6} failed with status ${res.status}`;
723
+ let message = `API ${method} ${path7} failed with status ${res.status}`;
586
724
  let code;
587
725
  try {
588
726
  const body = await res.json();
@@ -616,14 +754,14 @@ async function request(method, path6, options) {
616
754
  }
617
755
  throw lastError;
618
756
  }
619
- async function apiGet(path6, params) {
620
- return request("GET", path6, { params });
757
+ async function apiGet(path7, params) {
758
+ return request("GET", path7, { params });
621
759
  }
622
- async function apiPost(path6, body) {
623
- return request("POST", path6, { body });
760
+ async function apiPost(path7, body) {
761
+ return request("POST", path7, { body });
624
762
  }
625
- async function apiPut(path6, body) {
626
- return request("PUT", path6, { body });
763
+ async function apiPut(path7, body) {
764
+ return request("PUT", path7, { body });
627
765
  }
628
766
  async function callMcpTool(toolName, params) {
629
767
  const url = mcpEndpoint();
@@ -682,8 +820,8 @@ var init_api = __esm({
682
820
  });
683
821
 
684
822
  // src/lib/resolve-worktree.ts
685
- import { readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
686
- import { join as join4 } from "node:path";
823
+ import { readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
824
+ import { join as join5 } from "node:path";
687
825
  async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
688
826
  let worktreeId;
689
827
  try {
@@ -705,10 +843,10 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
705
843
  if (options?.skipWrite) {
706
844
  return worktreeId;
707
845
  }
708
- const codebyplanPath = join4(projectPath, ".codebyplan.json");
846
+ const codebyplanPath = join5(projectPath, ".codebyplan.json");
709
847
  let currentConfig = {};
710
848
  try {
711
- const raw = await readFile4(codebyplanPath, "utf-8");
849
+ const raw = await readFile5(codebyplanPath, "utf-8");
712
850
  const parsed = JSON.parse(raw);
713
851
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
714
852
  currentConfig = parsed;
@@ -723,7 +861,7 @@ async function resolveAndCacheWorktreeId(repoId, projectPath, options) {
723
861
  worktree_id: worktreeId
724
862
  };
725
863
  try {
726
- await writeFile4(
864
+ await writeFile5(
727
865
  codebyplanPath,
728
866
  JSON.stringify(merged, null, 2) + "\n",
729
867
  "utf-8"
@@ -880,7 +1018,7 @@ async function pollOnce(deviceCode, clientId) {
880
1018
  return { kind: "error", message: description };
881
1019
  }
882
1020
  async function pollUntilSettled(opts) {
883
- const sleep = opts.sleep ?? defaultSleep;
1021
+ const sleep2 = opts.sleep ?? defaultSleep;
884
1022
  const now = opts.now ?? Date.now;
885
1023
  const startMs = now();
886
1024
  const expiresAtMs = startMs + opts.expiresInSec * 1e3;
@@ -888,7 +1026,7 @@ async function pollUntilSettled(opts) {
888
1026
  const outcome = await pollOnce(opts.deviceCode, opts.clientId);
889
1027
  if (outcome.kind !== "pending") return outcome;
890
1028
  if (opts.onTick) opts.onTick(Math.floor((now() - startMs) / 1e3));
891
- await sleep(opts.intervalSec * 1e3);
1029
+ await sleep2(opts.intervalSec * 1e3);
892
1030
  }
893
1031
  return { kind: "expired" };
894
1032
  }
@@ -1057,21 +1195,21 @@ var setup_exports = {};
1057
1195
  __export(setup_exports, {
1058
1196
  runSetup: () => runSetup
1059
1197
  });
1060
- import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile5 } from "node:fs/promises";
1198
+ import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile6 } from "node:fs/promises";
1061
1199
  import { homedir as homedir2 } from "node:os";
1062
- import { join as join5 } from "node:path";
1200
+ import { join as join6 } from "node:path";
1063
1201
  import { stdin, stdout as stdout2 } from "node:process";
1064
1202
  import { createInterface } from "node:readline/promises";
1065
1203
  function getConfigPath(scope) {
1066
- return scope === "user" ? join5(homedir2(), ".claude.json") : join5(process.cwd(), ".mcp.json");
1204
+ return scope === "user" ? join6(homedir2(), ".claude.json") : join6(process.cwd(), ".mcp.json");
1067
1205
  }
1068
1206
  function legacyMcpUrl() {
1069
1207
  const baseUrl2 = process.env.CODEBYPLAN_API_URL ?? "https://www.codebyplan.com";
1070
1208
  return `${baseUrl2.replace(/\/$/, "")}/mcp`;
1071
1209
  }
1072
- async function readConfig(path6) {
1210
+ async function readConfig(path7) {
1073
1211
  try {
1074
- const raw = await readFile5(path6, "utf-8");
1212
+ const raw = await readFile6(path7, "utf-8");
1075
1213
  const parsed = JSON.parse(raw);
1076
1214
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1077
1215
  return parsed;
@@ -1094,7 +1232,7 @@ async function writeMcpConfig(scope, auth) {
1094
1232
  config.mcpServers = {};
1095
1233
  }
1096
1234
  config.mcpServers.codebyplan = buildMcpEntry(auth);
1097
- await writeFile5(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1235
+ await writeFile6(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
1098
1236
  return configPath;
1099
1237
  }
1100
1238
  async function fetchRepos(auth) {
@@ -1149,7 +1287,7 @@ async function chooseAuthMode(rl) {
1149
1287
  return { kind: "legacy", apiKey };
1150
1288
  }
1151
1289
  async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1152
- const codebyplanDir = join5(projectPath, ".codebyplan");
1290
+ const codebyplanDir = join6(projectPath, ".codebyplan");
1153
1291
  await mkdir4(codebyplanDir, { recursive: true });
1154
1292
  const repoJson = {
1155
1293
  repo_id: selectedRepo.id
@@ -1161,13 +1299,13 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1161
1299
  if (typeof repoAny.project_id === "string") {
1162
1300
  repoJson.project_id = repoAny.project_id;
1163
1301
  }
1164
- await writeFile5(
1165
- join5(codebyplanDir, "repo.json"),
1302
+ await writeFile6(
1303
+ join6(codebyplanDir, "repo.json"),
1166
1304
  JSON.stringify(repoJson, null, 2) + "\n",
1167
1305
  "utf-8"
1168
1306
  );
1169
- await writeFile5(
1170
- join5(codebyplanDir, "server.json"),
1307
+ await writeFile6(
1308
+ join6(codebyplanDir, "server.json"),
1171
1309
  JSON.stringify(
1172
1310
  {
1173
1311
  server_port: null,
@@ -1180,30 +1318,35 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1180
1318
  ) + "\n",
1181
1319
  "utf-8"
1182
1320
  );
1183
- await writeFile5(
1184
- join5(codebyplanDir, "git.json"),
1321
+ await writeFile6(
1322
+ join6(codebyplanDir, "git.json"),
1185
1323
  JSON.stringify({ git_branch: null, branch_config: null }, null, 2) + "\n",
1186
1324
  "utf-8"
1187
1325
  );
1188
- await writeFile5(
1189
- join5(codebyplanDir, "shipment.json"),
1326
+ await writeFile6(
1327
+ join6(codebyplanDir, "shipment.json"),
1190
1328
  JSON.stringify({ shipment: null }, null, 2) + "\n",
1191
1329
  "utf-8"
1192
1330
  );
1193
- await writeFile5(
1194
- join5(codebyplanDir, "vendor.json"),
1331
+ await writeFile6(
1332
+ join6(codebyplanDir, "vendor.json"),
1195
1333
  JSON.stringify({}, null, 2) + "\n",
1196
1334
  "utf-8"
1197
1335
  );
1198
- const statuslinePath = join5(codebyplanDir, "statusline.json");
1336
+ await writeFile6(
1337
+ join6(codebyplanDir, "e2e.json"),
1338
+ JSON.stringify({}, null, 2) + "\n",
1339
+ "utf-8"
1340
+ );
1341
+ const statuslinePath = join6(codebyplanDir, "statusline.json");
1199
1342
  let statuslineExists = false;
1200
1343
  try {
1201
- await readFile5(statuslinePath, "utf-8");
1344
+ await readFile6(statuslinePath, "utf-8");
1202
1345
  statuslineExists = true;
1203
1346
  } catch {
1204
1347
  }
1205
1348
  if (!statuslineExists) {
1206
- await writeFile5(
1349
+ await writeFile6(
1207
1350
  statuslinePath,
1208
1351
  JSON.stringify(STATUSLINE_DEFAULTS, null, 2) + "\n",
1209
1352
  "utf-8"
@@ -1212,36 +1355,12 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1212
1355
  await writeLocalConfig(projectPath, { device_id: deviceId });
1213
1356
  console.log(` Created ${codebyplanDir}/`);
1214
1357
  console.log(
1215
- ` repo.json, server.json, git.json, shipment.json, vendor.json, statusline.json`
1358
+ ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, statusline.json`
1216
1359
  );
1217
1360
  console.log(` device.local.json (gitignored)`);
1218
- const gitignorePath = join5(projectPath, ".gitignore");
1219
- const gitignoreEntry = ".codebyplan/device.local.json";
1220
- const statuslineLocalEntry = ".codebyplan/statusline.local.json";
1221
- try {
1222
- const existing = await readFile5(gitignorePath, "utf-8");
1223
- const lines = existing.split("\n");
1224
- let content = existing;
1225
- if (!lines.some((l) => l.trimEnd() === gitignoreEntry)) {
1226
- content = (content.endsWith("\n") ? content : content + "\n") + gitignoreEntry + "\n";
1227
- console.log(` Added '${gitignoreEntry}' to .gitignore`);
1228
- }
1229
- if (!lines.some((l) => l.trimEnd() === statuslineLocalEntry)) {
1230
- content = (content.endsWith("\n") ? content : content + "\n") + statuslineLocalEntry + "\n";
1231
- console.log(` Added '${statuslineLocalEntry}' to .gitignore`);
1232
- }
1233
- if (content !== existing) {
1234
- await writeFile5(gitignorePath, content, "utf-8");
1235
- }
1236
- } catch {
1237
- await writeFile5(
1238
- gitignorePath,
1239
- gitignoreEntry + "\n" + statuslineLocalEntry + "\n",
1240
- "utf-8"
1241
- );
1242
- console.log(
1243
- ` Created .gitignore with '${gitignoreEntry}' and '${statuslineLocalEntry}'`
1244
- );
1361
+ const gitignoreAction = await ensureManagedGitignoreBlock(projectPath);
1362
+ if (gitignoreAction !== "unchanged") {
1363
+ console.log(" Updated .gitignore (codebyplan managed block)");
1245
1364
  }
1246
1365
  }
1247
1366
  async function runSetup() {
@@ -1372,6 +1491,7 @@ async function runSetup() {
1372
1491
  var init_setup = __esm({
1373
1492
  "src/cli/setup.ts"() {
1374
1493
  "use strict";
1494
+ init_gitignore_block();
1375
1495
  init_local_config();
1376
1496
  init_statusline_config();
1377
1497
  init_resolve_worktree();
@@ -1382,21 +1502,21 @@ var init_setup = __esm({
1382
1502
  });
1383
1503
 
1384
1504
  // src/lib/flags.ts
1385
- import { readFile as readFile6 } from "node:fs/promises";
1386
- import { join as join6, resolve } from "node:path";
1505
+ import { readFile as readFile7 } from "node:fs/promises";
1506
+ import { join as join7, resolve } from "node:path";
1387
1507
  async function findCodebyplanConfig(startDir, maxDepth = 20) {
1388
1508
  let cursor = resolve(startDir);
1389
1509
  for (let depth = 0; depth < maxDepth; depth++) {
1390
- const sentinelPath2 = join6(cursor, ".codebyplan", "repo.json");
1510
+ const sentinelPath2 = join7(cursor, ".codebyplan", "repo.json");
1391
1511
  try {
1392
- const raw = await readFile6(sentinelPath2, "utf-8");
1512
+ const raw = await readFile7(sentinelPath2, "utf-8");
1393
1513
  const parsed = JSON.parse(raw);
1394
1514
  return { path: sentinelPath2, contents: parsed };
1395
1515
  } catch {
1396
1516
  }
1397
- const legacyPath = join6(cursor, ".codebyplan.json");
1517
+ const legacyPath = join7(cursor, ".codebyplan.json");
1398
1518
  try {
1399
- const raw = await readFile6(legacyPath, "utf-8");
1519
+ const raw = await readFile7(legacyPath, "utf-8");
1400
1520
  const parsed = JSON.parse(raw);
1401
1521
  return { path: legacyPath, contents: parsed };
1402
1522
  } catch {
@@ -1623,15 +1743,15 @@ var upgrade_auth_exports = {};
1623
1743
  __export(upgrade_auth_exports, {
1624
1744
  runUpgradeAuth: () => runUpgradeAuth
1625
1745
  });
1626
- import { readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
1746
+ import { readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
1627
1747
  import { homedir as homedir3 } from "node:os";
1628
- import { join as join7 } from "node:path";
1748
+ import { join as join8 } from "node:path";
1629
1749
  function configPaths() {
1630
- return [join7(homedir3(), ".claude.json"), join7(process.cwd(), ".mcp.json")];
1750
+ return [join8(homedir3(), ".claude.json"), join8(process.cwd(), ".mcp.json")];
1631
1751
  }
1632
- async function readConfig2(path6) {
1752
+ async function readConfig2(path7) {
1633
1753
  try {
1634
- const raw = await readFile7(path6, "utf-8");
1754
+ const raw = await readFile8(path7, "utf-8");
1635
1755
  const parsed = JSON.parse(raw);
1636
1756
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1637
1757
  return parsed;
@@ -1645,14 +1765,14 @@ function entryHasLegacyApiKey(entry) {
1645
1765
  if (!entry || !entry.headers) return false;
1646
1766
  return "x-api-key" in entry.headers;
1647
1767
  }
1648
- async function rewriteConfig(path6, config, newUrl) {
1768
+ async function rewriteConfig(path7, config, newUrl) {
1649
1769
  const servers = config.mcpServers;
1650
1770
  if (!servers) return false;
1651
1771
  const entry = servers.codebyplan;
1652
1772
  if (!entry) return false;
1653
1773
  if (!entryHasLegacyApiKey(entry) && entry.url === newUrl) return false;
1654
1774
  servers.codebyplan = { url: newUrl };
1655
- await writeFile6(path6, JSON.stringify(config, null, 2) + "\n", "utf-8");
1775
+ await writeFile7(path7, JSON.stringify(config, null, 2) + "\n", "utf-8");
1656
1776
  return true;
1657
1777
  }
1658
1778
  async function runUpgradeAuth() {
@@ -1660,12 +1780,12 @@ async function runUpgradeAuth() {
1660
1780
  await runLogin();
1661
1781
  const newUrl = mcpEndpoint();
1662
1782
  let migrated = 0;
1663
- for (const path6 of configPaths()) {
1664
- const config = await readConfig2(path6);
1783
+ for (const path7 of configPaths()) {
1784
+ const config = await readConfig2(path7);
1665
1785
  if (!config) continue;
1666
- const changed = await rewriteConfig(path6, config, newUrl);
1786
+ const changed = await rewriteConfig(path7, config, newUrl);
1667
1787
  if (changed) {
1668
- console.log(` Updated ${path6}`);
1788
+ console.log(` Updated ${path7}`);
1669
1789
  migrated++;
1670
1790
  }
1671
1791
  }
@@ -1733,8 +1853,8 @@ var init_confirm = __esm({
1733
1853
  });
1734
1854
 
1735
1855
  // src/lib/tech-detect.ts
1736
- import { readFile as readFile8, access, readdir } from "node:fs/promises";
1737
- import { join as join8, relative } from "node:path";
1856
+ import { readFile as readFile9, access, readdir } from "node:fs/promises";
1857
+ import { join as join9, relative } from "node:path";
1738
1858
  async function fileExists(filePath) {
1739
1859
  try {
1740
1860
  await access(filePath);
@@ -1747,8 +1867,8 @@ async function discoverMonorepoApps(projectPath) {
1747
1867
  const apps = [];
1748
1868
  const patterns = [];
1749
1869
  try {
1750
- const raw = await readFile8(
1751
- join8(projectPath, "pnpm-workspace.yaml"),
1870
+ const raw = await readFile9(
1871
+ join9(projectPath, "pnpm-workspace.yaml"),
1752
1872
  "utf-8"
1753
1873
  );
1754
1874
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1762,7 +1882,7 @@ async function discoverMonorepoApps(projectPath) {
1762
1882
  }
1763
1883
  if (patterns.length === 0) {
1764
1884
  try {
1765
- const raw = await readFile8(join8(projectPath, "package.json"), "utf-8");
1885
+ const raw = await readFile9(join9(projectPath, "package.json"), "utf-8");
1766
1886
  const pkg = JSON.parse(raw);
1767
1887
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1768
1888
  if (ws) patterns.push(...ws);
@@ -1772,14 +1892,14 @@ async function discoverMonorepoApps(projectPath) {
1772
1892
  for (const pattern of patterns) {
1773
1893
  if (pattern.endsWith("/*")) {
1774
1894
  const dir = pattern.slice(0, -2);
1775
- const absDir = join8(projectPath, dir);
1895
+ const absDir = join9(projectPath, dir);
1776
1896
  try {
1777
1897
  const entries = await readdir(absDir, { withFileTypes: true });
1778
1898
  for (const entry of entries) {
1779
1899
  if (entry.isDirectory()) {
1780
- const relPath = join8(dir, entry.name);
1781
- const absPath = join8(absDir, entry.name);
1782
- if (await fileExists(join8(absPath, "package.json"))) {
1900
+ const relPath = join9(dir, entry.name);
1901
+ const absPath = join9(absDir, entry.name);
1902
+ if (await fileExists(join9(absPath, "package.json"))) {
1783
1903
  apps.push({ name: entry.name, path: relPath, absPath });
1784
1904
  }
1785
1905
  }
@@ -1798,7 +1918,7 @@ async function hasJsxFile(dir, depth = 0) {
1798
1918
  const name = entry.name;
1799
1919
  if (entry.isDirectory()) {
1800
1920
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1801
- if (await hasJsxFile(join8(dir, name), depth + 1)) return true;
1921
+ if (await hasJsxFile(join9(dir, name), depth + 1)) return true;
1802
1922
  } else if (entry.isFile()) {
1803
1923
  if (JSX_TEST_PATTERN.test(name)) continue;
1804
1924
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1817,7 +1937,7 @@ async function hasJsxFile(dir, depth = 0) {
1817
1937
  async function detectCapabilities(dirPath, pkgJson) {
1818
1938
  const caps = /* @__PURE__ */ new Set();
1819
1939
  for (const sub of JSX_SCAN_DIRS) {
1820
- if (await hasJsxFile(join8(dirPath, sub))) {
1940
+ if (await hasJsxFile(join9(dirPath, sub))) {
1821
1941
  caps.add("jsx");
1822
1942
  break;
1823
1943
  }
@@ -1839,7 +1959,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1839
1959
  }
1840
1960
  }
1841
1961
  }
1842
- if (!caps.has("node-server") && await fileExists(join8(dirPath, "src", "main.ts"))) {
1962
+ if (!caps.has("node-server") && await fileExists(join9(dirPath, "src", "main.ts"))) {
1843
1963
  caps.add("node-server");
1844
1964
  }
1845
1965
  if (pkgJson && pkgJson.bin) {
@@ -1855,7 +1975,7 @@ async function detectFromDirectory(dirPath) {
1855
1975
  const seen = /* @__PURE__ */ new Map();
1856
1976
  let pkgJson = null;
1857
1977
  try {
1858
- const raw = await readFile8(join8(dirPath, "package.json"), "utf-8");
1978
+ const raw = await readFile9(join9(dirPath, "package.json"), "utf-8");
1859
1979
  pkgJson = JSON.parse(raw);
1860
1980
  const allDeps = {
1861
1981
  ...pkgJson.dependencies ?? {},
@@ -1887,7 +2007,7 @@ async function detectFromDirectory(dirPath) {
1887
2007
  }
1888
2008
  for (const { file, rule } of CONFIG_FILE_MAP) {
1889
2009
  const key = rule.name.toLowerCase();
1890
- if (!seen.has(key) && await fileExists(join8(dirPath, file))) {
2010
+ if (!seen.has(key) && await fileExists(join9(dirPath, file))) {
1891
2011
  seen.set(key, { name: rule.name, category: rule.category });
1892
2012
  }
1893
2013
  }
@@ -2065,7 +2185,7 @@ function categorizeDependency(depName) {
2065
2185
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
2066
2186
  if (depth > 4) return [];
2067
2187
  const results = [];
2068
- const pkgPath = join8(dir, "package.json");
2188
+ const pkgPath = join9(dir, "package.json");
2069
2189
  if (await fileExists(pkgPath)) {
2070
2190
  results.push(pkgPath);
2071
2191
  }
@@ -2074,7 +2194,7 @@ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
2074
2194
  for (const entry of entries) {
2075
2195
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
2076
2196
  const subResults = await findPackageJsonFiles(
2077
- join8(dir, entry.name),
2197
+ join9(dir, entry.name),
2078
2198
  projectPath,
2079
2199
  depth + 1
2080
2200
  );
@@ -2089,7 +2209,7 @@ async function scanAllDependencies(projectPath) {
2089
2209
  const dependencies = [];
2090
2210
  for (const pkgPath of packageJsonPaths) {
2091
2211
  try {
2092
- const raw = await readFile8(pkgPath, "utf-8");
2212
+ const raw = await readFile9(pkgPath, "utf-8");
2093
2213
  const pkg = JSON.parse(raw);
2094
2214
  const sourcePath = relative(projectPath, pkgPath);
2095
2215
  const depSections = [
@@ -2161,6 +2281,9 @@ var init_tech_detect = __esm({
2161
2281
  "@playwright/test": { name: "Playwright", category: "testing" },
2162
2282
  cypress: { name: "Cypress", category: "testing" },
2163
2283
  supertest: { name: "Supertest", category: "testing" },
2284
+ webdriverio: { name: "WebdriverIO", category: "testing" },
2285
+ "@wdio/cli": { name: "WebdriverIO", category: "testing" },
2286
+ "@vscode/test-cli": { name: "VS Code Test CLI", category: "testing" },
2164
2287
  // Build tools
2165
2288
  turbo: { name: "Turborepo", category: "build" },
2166
2289
  vite: { name: "Vite", category: "build" },
@@ -2245,7 +2368,28 @@ var init_tech_detect = __esm({
2245
2368
  rule: { name: "shadcn/ui", category: "component-lib" }
2246
2369
  },
2247
2370
  { file: "nx.json", rule: { name: "Nx", category: "build" } },
2248
- { file: "lerna.json", rule: { name: "Lerna", category: "build" } }
2371
+ { file: "lerna.json", rule: { name: "Lerna", category: "build" } },
2372
+ {
2373
+ file: "playwright.config.ts",
2374
+ rule: { name: "Playwright", category: "testing" }
2375
+ },
2376
+ {
2377
+ file: "playwright.config.js",
2378
+ rule: { name: "Playwright", category: "testing" }
2379
+ },
2380
+ {
2381
+ file: "playwright.config.mjs",
2382
+ rule: { name: "Playwright", category: "testing" }
2383
+ },
2384
+ { file: "wdio.conf.ts", rule: { name: "WebdriverIO", category: "testing" } },
2385
+ {
2386
+ file: "wdio.conf.js",
2387
+ rule: { name: "WebdriverIO", category: "testing" }
2388
+ },
2389
+ {
2390
+ file: "maestro/config.yaml",
2391
+ rule: { name: "Maestro", category: "testing" }
2392
+ }
2249
2393
  ];
2250
2394
  SYNTHETIC_CARRIER_NAME = "__capabilities__";
2251
2395
  CAPABILITY_BEARER_NAMES = /* @__PURE__ */ new Set([
@@ -2689,8 +2833,8 @@ __export(eslint_exports, {
2689
2833
  eslintInit: () => eslintInit,
2690
2834
  runEslint: () => runEslint
2691
2835
  });
2692
- import { readFile as readFile9, writeFile as writeFile7, access as access2, readdir as readdir2 } from "node:fs/promises";
2693
- import { join as join9, relative as relative2 } from "node:path";
2836
+ import { readFile as readFile10, writeFile as writeFile8, access as access2, readdir as readdir2 } from "node:fs/promises";
2837
+ import { join as join10, relative as relative2 } from "node:path";
2694
2838
  async function fileExists2(filePath) {
2695
2839
  try {
2696
2840
  await access2(filePath);
@@ -2701,7 +2845,7 @@ async function fileExists2(filePath) {
2701
2845
  }
2702
2846
  async function autoDetectIgnorePatterns(absPath) {
2703
2847
  const patterns = [];
2704
- if (await fileExists2(join9(absPath, "esbuild.js"))) {
2848
+ if (await fileExists2(join10(absPath, "esbuild.js"))) {
2705
2849
  patterns.push("esbuild.js");
2706
2850
  }
2707
2851
  let entries = [];
@@ -2721,19 +2865,19 @@ async function autoDetectIgnorePatterns(absPath) {
2721
2865
  }
2722
2866
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2723
2867
  const candidate = `vitest.config.${ext}`;
2724
- if (await fileExists2(join9(absPath, candidate))) {
2868
+ if (await fileExists2(join10(absPath, candidate))) {
2725
2869
  patterns.push(candidate);
2726
2870
  break;
2727
2871
  }
2728
2872
  }
2729
2873
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2730
2874
  const candidate = `vite.config.${ext}`;
2731
- if (await fileExists2(join9(absPath, candidate))) {
2875
+ if (await fileExists2(join10(absPath, candidate))) {
2732
2876
  patterns.push(candidate);
2733
2877
  break;
2734
2878
  }
2735
2879
  }
2736
- if (await fileExists2(join9(absPath, "tauri.conf.json"))) {
2880
+ if (await fileExists2(join10(absPath, "tauri.conf.json"))) {
2737
2881
  patterns.push("src-tauri/**");
2738
2882
  patterns.push("**/*.d.ts");
2739
2883
  }
@@ -2741,14 +2885,14 @@ async function autoDetectIgnorePatterns(absPath) {
2741
2885
  }
2742
2886
  function detectPackageManager(projectPath) {
2743
2887
  return (async () => {
2744
- if (await fileExists2(join9(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2745
- if (await fileExists2(join9(projectPath, "yarn.lock"))) return "yarn";
2888
+ if (await fileExists2(join10(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2889
+ if (await fileExists2(join10(projectPath, "yarn.lock"))) return "yarn";
2746
2890
  return "npm";
2747
2891
  })();
2748
2892
  }
2749
2893
  async function getInstalledDeps(pkgJsonPath) {
2750
2894
  try {
2751
- const raw = await readFile9(pkgJsonPath, "utf-8");
2895
+ const raw = await readFile10(pkgJsonPath, "utf-8");
2752
2896
  const pkg = JSON.parse(raw);
2753
2897
  const all = /* @__PURE__ */ new Set();
2754
2898
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -2861,7 +3005,7 @@ async function eslintInit(repoId, projectPath) {
2861
3005
  ignorePatterns: detectedIgnores
2862
3006
  });
2863
3007
  const hash = hashConfig(content);
2864
- const configPath = join9(target.absPath, "eslint.config.mjs");
3008
+ const configPath = join10(target.absPath, "eslint.config.mjs");
2865
3009
  configsToWrite.push({
2866
3010
  target,
2867
3011
  presets,
@@ -2883,11 +3027,11 @@ async function eslintInit(repoId, projectPath) {
2883
3027
  return;
2884
3028
  }
2885
3029
  const pm = await detectPackageManager(projectPath);
2886
- const rootPkgJsonPath = join9(projectPath, "package.json");
3030
+ const rootPkgJsonPath = join10(projectPath, "package.json");
2887
3031
  const installed = await getInstalledDeps(rootPkgJsonPath);
2888
3032
  if (isMonorepo) {
2889
3033
  for (const { target } of configsToWrite) {
2890
- const appPkgJson = join9(target.absPath, "package.json");
3034
+ const appPkgJson = join10(target.absPath, "package.json");
2891
3035
  const appDeps = await getInstalledDeps(appPkgJson);
2892
3036
  for (const dep of appDeps) {
2893
3037
  installed.add(dep);
@@ -2939,7 +3083,7 @@ async function eslintInit(repoId, projectPath) {
2939
3083
  } of configsToWrite) {
2940
3084
  if (await fileExists2(configPath)) {
2941
3085
  try {
2942
- const existing = await readFile9(configPath, "utf-8");
3086
+ const existing = await readFile10(configPath, "utf-8");
2943
3087
  const existingHash = hashConfig(existing);
2944
3088
  if (existingHash === hash) {
2945
3089
  console.log(
@@ -2959,7 +3103,7 @@ async function eslintInit(repoId, projectPath) {
2959
3103
  }
2960
3104
  }
2961
3105
  try {
2962
- await writeFile7(configPath, content, "utf-8");
3106
+ await writeFile8(configPath, content, "utf-8");
2963
3107
  } catch (err) {
2964
3108
  console.error(
2965
3109
  ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -3263,12 +3407,48 @@ var init_sync_approvals = __esm({
3263
3407
  // src/cli/round.ts
3264
3408
  var round_exports = {};
3265
3409
  __export(round_exports, {
3410
+ RETRY_DELAY_MS: () => RETRY_DELAY_MS,
3411
+ fetchRoundsWithRetry: () => fetchRoundsWithRetry,
3412
+ isTransientMcpError: () => isTransientMcpError,
3266
3413
  runRoundCommand: () => runRoundCommand,
3267
- runRoundSyncApprovals: () => runRoundSyncApprovals
3414
+ runRoundSyncApprovals: () => runRoundSyncApprovals,
3415
+ setRetryDelayMs: () => setRetryDelayMs
3268
3416
  });
3269
3417
  import { access as access3 } from "node:fs/promises";
3270
- import { join as join10 } from "node:path";
3418
+ import { join as join11 } from "node:path";
3271
3419
  import { execSync as execSync2 } from "node:child_process";
3420
+ function setRetryDelayMs(ms) {
3421
+ RETRY_DELAY_MS = ms;
3422
+ }
3423
+ function sleep(ms) {
3424
+ return new Promise((resolve5) => setTimeout(resolve5, ms));
3425
+ }
3426
+ function isTransientMcpError(err) {
3427
+ if (!(err instanceof McpError)) return false;
3428
+ if (err.status !== void 0) {
3429
+ return err.status >= 500 && err.status <= 599;
3430
+ }
3431
+ return err.message.toLowerCase().includes("network error");
3432
+ }
3433
+ async function fetchRoundsWithRetry(taskId, options = {}) {
3434
+ const maxRetries = options.retries ?? 2;
3435
+ const delayMs = options.delayMs ?? RETRY_DELAY_MS;
3436
+ let lastErr;
3437
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
3438
+ try {
3439
+ return await mcpCall("get_rounds", { task_id: taskId });
3440
+ } catch (err) {
3441
+ if (!isTransientMcpError(err)) {
3442
+ throw err;
3443
+ }
3444
+ lastErr = err;
3445
+ if (attempt < maxRetries) {
3446
+ await sleep(delayMs);
3447
+ }
3448
+ }
3449
+ }
3450
+ throw lastErr;
3451
+ }
3272
3452
  async function runRoundCommand(args) {
3273
3453
  const subcommand = args[0];
3274
3454
  if (subcommand === "sync-approvals") {
@@ -3330,6 +3510,7 @@ async function runRoundSyncApprovals(args) {
3330
3510
  );
3331
3511
  process.exit(1);
3332
3512
  }
3513
+ let skipReason = null;
3333
3514
  let stdoutPayload = null;
3334
3515
  try {
3335
3516
  let callerWorktreeId = flags["worktree-id"];
@@ -3341,84 +3522,95 @@ async function runRoundSyncApprovals(args) {
3341
3522
  // Walk up to the directory containing .codebyplan/ or .codebyplan.json
3342
3523
  found.path.replace(/\/.codebyplan(\.json|\/repo\.json)$/, "")
3343
3524
  ) : process.cwd();
3344
- const rounds = await mcpCall("get_rounds", {
3345
- task_id: taskId
3346
- });
3347
- const currentRound = rounds.find((r) => r.id === roundId);
3348
- if (!currentRound) {
3349
- throw new Error(`Round ${roundId} not found for task ${taskId}`);
3350
- }
3351
- const taskResponse = await apiGet(`/tasks/${taskId}`);
3352
- const currentTask = taskResponse.data;
3353
- let gitStatusOutput = "";
3354
- try {
3355
- gitStatusOutput = execSync2("git status --short --porcelain -z", {
3356
- cwd: repoRoot,
3357
- encoding: "utf-8"
3358
- });
3359
- } catch {
3360
- process.stderr.write(
3361
- "sync-approvals: git status failed; proceeding with empty diff\n"
3362
- );
3363
- }
3364
- const hookPath = join10(
3365
- repoRoot,
3366
- ".claude",
3367
- "hooks",
3368
- "lint-format-on-edit.sh"
3369
- );
3370
- let lintFormatHookExists = false;
3525
+ let rounds;
3371
3526
  try {
3372
- await access3(hookPath);
3373
- lintFormatHookExists = true;
3374
- } catch {
3527
+ rounds = await fetchRoundsWithRetry(taskId);
3528
+ } catch (err) {
3529
+ if (isTransientMcpError(err)) {
3530
+ const reason = err instanceof McpError ? err.message : err instanceof Error ? err.message : String(err);
3531
+ skipReason = reason;
3532
+ rounds = [];
3533
+ } else {
3534
+ throw err;
3535
+ }
3375
3536
  }
3376
- const result = runSyncApprovals({
3377
- currentRound,
3378
- currentTask,
3379
- gitStatusOutput,
3380
- lintFormatHookExists
3381
- });
3382
- if (dryRun) {
3383
- stdoutPayload = JSON.stringify(
3384
- {
3385
- added: result.added,
3386
- stale_marked: result.stale_marked,
3387
- reactivated: result.reactivated,
3388
- total_files: result.total_files,
3389
- merged_files_changed: result.merged_files_changed
3390
- },
3391
- null,
3392
- 2
3393
- );
3394
- } else {
3395
- const roundArgs = {
3396
- round_id: roundId,
3397
- files_changed: result.merged_files_changed
3398
- };
3399
- if (callerWorktreeId) {
3400
- roundArgs["caller_worktree_id"] = callerWorktreeId;
3537
+ if (!skipReason) {
3538
+ const currentRound = rounds.find((r) => r.id === roundId);
3539
+ if (!currentRound) {
3540
+ throw new Error(`Round ${roundId} not found for task ${taskId}`);
3401
3541
  }
3402
- await mcpCall("update_round", roundArgs);
3403
- const taskArgs = {
3404
- task_id: taskId,
3405
- files_changed: result.merged_files_changed,
3406
- app_file_approval_by_user: false
3407
- };
3408
- if (callerWorktreeId) {
3409
- taskArgs["caller_worktree_id"] = callerWorktreeId;
3542
+ const taskResponse = await apiGet(`/tasks/${taskId}`);
3543
+ const currentTask = taskResponse.data;
3544
+ let gitStatusOutput = "";
3545
+ try {
3546
+ gitStatusOutput = execSync2("git status --short --porcelain -z", {
3547
+ cwd: repoRoot,
3548
+ encoding: "utf-8"
3549
+ });
3550
+ } catch {
3551
+ process.stderr.write(
3552
+ "sync-approvals: git status failed; proceeding with empty diff\n"
3553
+ );
3410
3554
  }
3411
- await mcpCall("update_task", taskArgs);
3412
- stdoutPayload = JSON.stringify(
3413
- {
3414
- added: result.added,
3415
- stale_marked: result.stale_marked,
3416
- reactivated: result.reactivated,
3417
- total_files: result.total_files
3418
- },
3419
- null,
3420
- 2
3555
+ const hookPath = join11(
3556
+ repoRoot,
3557
+ ".claude",
3558
+ "hooks",
3559
+ "lint-format-on-edit.sh"
3421
3560
  );
3561
+ let lintFormatHookExists = false;
3562
+ try {
3563
+ await access3(hookPath);
3564
+ lintFormatHookExists = true;
3565
+ } catch {
3566
+ }
3567
+ const result = runSyncApprovals({
3568
+ currentRound,
3569
+ currentTask,
3570
+ gitStatusOutput,
3571
+ lintFormatHookExists
3572
+ });
3573
+ if (dryRun) {
3574
+ stdoutPayload = JSON.stringify(
3575
+ {
3576
+ added: result.added,
3577
+ stale_marked: result.stale_marked,
3578
+ reactivated: result.reactivated,
3579
+ total_files: result.total_files,
3580
+ merged_files_changed: result.merged_files_changed
3581
+ },
3582
+ null,
3583
+ 2
3584
+ );
3585
+ } else {
3586
+ const roundArgs = {
3587
+ round_id: roundId,
3588
+ 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 = {
3595
+ task_id: taskId,
3596
+ files_changed: result.merged_files_changed,
3597
+ app_file_approval_by_user: false
3598
+ };
3599
+ if (callerWorktreeId) {
3600
+ taskArgs["caller_worktree_id"] = callerWorktreeId;
3601
+ }
3602
+ await mcpCall("update_task", taskArgs);
3603
+ stdoutPayload = JSON.stringify(
3604
+ {
3605
+ added: result.added,
3606
+ stale_marked: result.stale_marked,
3607
+ reactivated: result.reactivated,
3608
+ total_files: result.total_files
3609
+ },
3610
+ null,
3611
+ 2
3612
+ );
3613
+ }
3422
3614
  }
3423
3615
  } catch (err) {
3424
3616
  process.stderr.write(
@@ -3427,6 +3619,13 @@ async function runRoundSyncApprovals(args) {
3427
3619
  );
3428
3620
  process.exit(1);
3429
3621
  }
3622
+ if (skipReason !== null) {
3623
+ process.stderr.write(
3624
+ `sync-approvals: MCP temporarily unavailable (${skipReason}); skipping approval sync. Re-run /cbp-round-update when the service recovers.
3625
+ `
3626
+ );
3627
+ process.exit(0);
3628
+ }
3430
3629
  if (stdoutPayload === null) {
3431
3630
  process.stderr.write("sync-approvals: internal error \u2014 payload not set\n");
3432
3631
  process.exit(1);
@@ -3470,6 +3669,7 @@ async function autoResolveWorktreeId() {
3470
3669
  return void 0;
3471
3670
  }
3472
3671
  }
3672
+ var RETRY_DELAY_MS;
3473
3673
  var init_round = __esm({
3474
3674
  "src/cli/round.ts"() {
3475
3675
  "use strict";
@@ -3479,12 +3679,13 @@ var init_round = __esm({
3479
3679
  init_resolve_worktree();
3480
3680
  init_local_config();
3481
3681
  init_sync_approvals();
3682
+ RETRY_DELAY_MS = 1e3;
3482
3683
  }
3483
3684
  });
3484
3685
 
3485
3686
  // src/lib/migrate-branch-model.ts
3486
- import { readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
3487
- import { join as join11 } from "node:path";
3687
+ import { readFile as readFile11, writeFile as writeFile9 } from "node:fs/promises";
3688
+ import { join as join12 } from "node:path";
3488
3689
  import { execSync as execSync3 } from "node:child_process";
3489
3690
  function assertValidBranchName(branch) {
3490
3691
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3494,7 +3695,7 @@ function assertValidBranchName(branch) {
3494
3695
  }
3495
3696
  }
3496
3697
  async function readJsonFile(filePath) {
3497
- const raw = await readFile10(filePath, "utf-8");
3698
+ const raw = await readFile11(filePath, "utf-8");
3498
3699
  const parsed = JSON.parse(raw);
3499
3700
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3500
3701
  throw new Error(`${filePath} does not contain a JSON object`);
@@ -3563,12 +3764,12 @@ async function runBranchMigration(opts) {
3563
3764
  if (found) {
3564
3765
  if (found.path.endsWith("/repo.json")) {
3565
3766
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3566
- configPath = join11(dir, "git.json");
3767
+ configPath = join12(dir, "git.json");
3567
3768
  } else {
3568
3769
  configPath = found.path;
3569
3770
  }
3570
3771
  } else {
3571
- configPath = join11(cwd, ".codebyplan", "git.json");
3772
+ configPath = join12(cwd, ".codebyplan", "git.json");
3572
3773
  }
3573
3774
  let fileRaw;
3574
3775
  let fileParsed;
@@ -3642,7 +3843,7 @@ async function runBranchMigration(opts) {
3642
3843
  const updatedParsed = { ...fileParsed, branch_config: after };
3643
3844
  const newJson = JSON.stringify(updatedParsed, null, 2) + "\n";
3644
3845
  if (newJson !== fileRaw) {
3645
- await writeFile8(configPath, newJson, "utf-8");
3846
+ await writeFile9(configPath, newJson, "utf-8");
3646
3847
  }
3647
3848
  }
3648
3849
  return {
@@ -3748,8 +3949,8 @@ var init_branch = __esm({
3748
3949
  });
3749
3950
 
3750
3951
  // src/lib/ship.ts
3751
- import { readFile as readFile11 } from "node:fs/promises";
3752
- import { join as join12 } from "node:path";
3952
+ import { readFile as readFile12 } from "node:fs/promises";
3953
+ import { join as join13 } from "node:path";
3753
3954
  import { execSync as execSync4, spawnSync as spawnSync2 } from "node:child_process";
3754
3955
  function assertValidBranchName2(branch, label) {
3755
3956
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3764,15 +3965,15 @@ async function readBaseBranch(cwd) {
3764
3965
  if (found) {
3765
3966
  if (found.path.endsWith("/repo.json")) {
3766
3967
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3767
- gitJsonPath = join12(dir, "git.json");
3968
+ gitJsonPath = join13(dir, "git.json");
3768
3969
  } else {
3769
3970
  gitJsonPath = found.path;
3770
3971
  }
3771
3972
  } else {
3772
- gitJsonPath = join12(cwd, ".codebyplan", "git.json");
3973
+ gitJsonPath = join13(cwd, ".codebyplan", "git.json");
3773
3974
  }
3774
3975
  try {
3775
- const raw = await readFile11(gitJsonPath, "utf-8");
3976
+ const raw = await readFile12(gitJsonPath, "utf-8");
3776
3977
  const parsed = JSON.parse(raw);
3777
3978
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3778
3979
  return "main";
@@ -4304,19 +4505,19 @@ var init_resolve_worktree2 = __esm({
4304
4505
  });
4305
4506
 
4306
4507
  // src/lib/migrate-local-config.ts
4307
- import { mkdir as mkdir5, readFile as readFile12, unlink as unlink2, writeFile as writeFile9 } from "node:fs/promises";
4308
- import { join as join13 } from "node:path";
4508
+ import { mkdir as mkdir5, readFile as readFile13, unlink as unlink2, writeFile as writeFile10 } from "node:fs/promises";
4509
+ import { join as join14 } from "node:path";
4309
4510
  function legacySharedPath(projectPath) {
4310
- return join13(projectPath, ".codebyplan.json");
4511
+ return join14(projectPath, ".codebyplan.json");
4311
4512
  }
4312
4513
  function legacyLocalPath(projectPath) {
4313
- return join13(projectPath, ".codebyplan.local.json");
4514
+ return join14(projectPath, ".codebyplan.local.json");
4314
4515
  }
4315
4516
  function newDirPath(projectPath) {
4316
- return join13(projectPath, ".codebyplan");
4517
+ return join14(projectPath, ".codebyplan");
4317
4518
  }
4318
4519
  function sentinelPath(projectPath) {
4319
- return join13(projectPath, ".codebyplan", "repo.json");
4520
+ return join14(projectPath, ".codebyplan", "repo.json");
4320
4521
  }
4321
4522
  async function statSafe(p) {
4322
4523
  const { stat: stat2 } = await import("node:fs/promises");
@@ -4355,7 +4556,7 @@ async function runLocalMigration(projectPath) {
4355
4556
  }
4356
4557
  let legacyRaw;
4357
4558
  try {
4358
- legacyRaw = await readFile12(legacySharedPath(projectPath), "utf-8");
4559
+ legacyRaw = await readFile13(legacySharedPath(projectPath), "utf-8");
4359
4560
  } catch {
4360
4561
  return {
4361
4562
  migrated: true,
@@ -4382,7 +4583,7 @@ async function runLocalMigration(projectPath) {
4382
4583
  let deviceId;
4383
4584
  let deviceWrittenByHelper = false;
4384
4585
  try {
4385
- const localRaw = await readFile12(legacyLocalPath(projectPath), "utf-8");
4586
+ const localRaw = await readFile13(legacyLocalPath(projectPath), "utf-8");
4386
4587
  const localParsed = JSON.parse(localRaw);
4387
4588
  if (typeof localParsed.device_id === "string") {
4388
4589
  deviceId = localParsed.device_id;
@@ -4409,8 +4610,8 @@ async function runLocalMigration(projectPath) {
4409
4610
  if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
4410
4611
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
4411
4612
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
4412
- await writeFile9(
4413
- join13(projectPath, ".codebyplan", "repo.json"),
4613
+ await writeFile10(
4614
+ join14(projectPath, ".codebyplan", "repo.json"),
4414
4615
  JSON.stringify(repoJson, null, 2) + "\n",
4415
4616
  "utf-8"
4416
4617
  );
@@ -4422,8 +4623,8 @@ async function runLocalMigration(projectPath) {
4422
4623
  serverJson.auto_push_enabled = cfg.auto_push_enabled;
4423
4624
  if ("port_allocations" in cfg)
4424
4625
  serverJson.port_allocations = cfg.port_allocations;
4425
- await writeFile9(
4426
- join13(projectPath, ".codebyplan", "server.json"),
4626
+ await writeFile10(
4627
+ join14(projectPath, ".codebyplan", "server.json"),
4427
4628
  JSON.stringify(serverJson, null, 2) + "\n",
4428
4629
  "utf-8"
4429
4630
  );
@@ -4431,30 +4632,37 @@ async function runLocalMigration(projectPath) {
4431
4632
  const gitJson = {};
4432
4633
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
4433
4634
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
4434
- await writeFile9(
4435
- join13(projectPath, ".codebyplan", "git.json"),
4635
+ await writeFile10(
4636
+ join14(projectPath, ".codebyplan", "git.json"),
4436
4637
  JSON.stringify(gitJson, null, 2) + "\n",
4437
4638
  "utf-8"
4438
4639
  );
4439
4640
  filesChanged.push(".codebyplan/git.json");
4440
4641
  const shipmentJson = {};
4441
4642
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
4442
- await writeFile9(
4443
- join13(projectPath, ".codebyplan", "shipment.json"),
4643
+ await writeFile10(
4644
+ join14(projectPath, ".codebyplan", "shipment.json"),
4444
4645
  JSON.stringify(shipmentJson, null, 2) + "\n",
4445
4646
  "utf-8"
4446
4647
  );
4447
4648
  filesChanged.push(".codebyplan/shipment.json");
4448
4649
  const vendorJson = {};
4449
- await writeFile9(
4450
- join13(projectPath, ".codebyplan", "vendor.json"),
4650
+ await writeFile10(
4651
+ join14(projectPath, ".codebyplan", "vendor.json"),
4451
4652
  JSON.stringify(vendorJson, null, 2) + "\n",
4452
4653
  "utf-8"
4453
4654
  );
4454
4655
  filesChanged.push(".codebyplan/vendor.json");
4656
+ const e2eJson = {};
4657
+ await writeFile10(
4658
+ join14(projectPath, ".codebyplan", "e2e.json"),
4659
+ JSON.stringify(e2eJson, null, 2) + "\n",
4660
+ "utf-8"
4661
+ );
4662
+ filesChanged.push(".codebyplan/e2e.json");
4455
4663
  if (!deviceWrittenByHelper) {
4456
- await writeFile9(
4457
- join13(projectPath, ".codebyplan", "device.local.json"),
4664
+ await writeFile10(
4665
+ join14(projectPath, ".codebyplan", "device.local.json"),
4458
4666
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
4459
4667
  "utf-8"
4460
4668
  );
@@ -4466,9 +4674,9 @@ async function runLocalMigration(projectPath) {
4466
4674
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
4467
4675
  );
4468
4676
  }
4469
- const gitignorePath = join13(projectPath, ".gitignore");
4677
+ const gitignorePath = join14(projectPath, ".gitignore");
4470
4678
  try {
4471
- const gitignoreContent = await readFile12(gitignorePath, "utf-8");
4679
+ const gitignoreContent = await readFile13(gitignorePath, "utf-8");
4472
4680
  const legacyLine = ".codebyplan.local.json";
4473
4681
  const newLine = ".codebyplan/device.local.json";
4474
4682
  const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
@@ -4487,7 +4695,7 @@ async function runLocalMigration(projectPath) {
4487
4695
  updated = gitignoreContent;
4488
4696
  }
4489
4697
  if (updated !== gitignoreContent) {
4490
- await writeFile9(gitignorePath, updated, "utf-8");
4698
+ await writeFile10(gitignorePath, updated, "utf-8");
4491
4699
  filesChanged.push(".gitignore");
4492
4700
  }
4493
4701
  } catch {
@@ -4519,6 +4727,7 @@ var init_migrate_local_config = __esm({
4519
4727
  // src/cli/config.ts
4520
4728
  var config_exports = {};
4521
4729
  __export(config_exports, {
4730
+ readE2eConfig: () => readE2eConfig,
4522
4731
  readGitConfig: () => readGitConfig,
4523
4732
  readRepoConfig: () => readRepoConfig,
4524
4733
  readServerConfig: () => readServerConfig,
@@ -4526,8 +4735,8 @@ __export(config_exports, {
4526
4735
  readVendorConfig: () => readVendorConfig,
4527
4736
  runConfig: () => runConfig
4528
4737
  });
4529
- import { mkdir as mkdir6, readFile as readFile13, writeFile as writeFile10 } from "node:fs/promises";
4530
- import { join as join14 } from "node:path";
4738
+ import { mkdir as mkdir6, readFile as readFile14, writeFile as writeFile11 } from "node:fs/promises";
4739
+ import { join as join15 } from "node:path";
4531
4740
  async function runConfig() {
4532
4741
  const flags = parseFlags(3);
4533
4742
  const dryRun = hasFlag("dry-run", 3);
@@ -4560,7 +4769,7 @@ async function runConfig() {
4560
4769
  console.log("\n Config complete.\n");
4561
4770
  }
4562
4771
  async function syncConfigToFile(repoId, projectPath, dryRun) {
4563
- const codebyplanDir = join14(projectPath, ".codebyplan");
4772
+ const codebyplanDir = join15(projectPath, ".codebyplan");
4564
4773
  let resolvedWorktreeId;
4565
4774
  try {
4566
4775
  const deviceId = await getOrCreateDeviceId(projectPath);
@@ -4685,6 +4894,7 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4685
4894
  shipmentPayload.shipment = repoAny.shipment;
4686
4895
  }
4687
4896
  const vendorPayload = {};
4897
+ const e2ePayload = {};
4688
4898
  if (dryRun) {
4689
4899
  console.log(" Config would be updated (dry-run).");
4690
4900
  return;
@@ -4695,19 +4905,21 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4695
4905
  { name: "server.json", payload: serverPayload },
4696
4906
  { name: "git.json", payload: gitPayload },
4697
4907
  { name: "shipment.json", payload: shipmentPayload },
4698
- { name: "vendor.json", payload: vendorPayload }
4908
+ { name: "vendor.json", payload: vendorPayload },
4909
+ { name: "e2e.json", payload: e2ePayload, createOnly: true }
4699
4910
  ];
4700
4911
  let anyUpdated = false;
4701
- for (const { name, payload } of files) {
4702
- const filePath = join14(codebyplanDir, name);
4912
+ for (const { name, payload, createOnly } of files) {
4913
+ const filePath = join15(codebyplanDir, name);
4703
4914
  const newJson = JSON.stringify(payload, null, 2) + "\n";
4704
4915
  let currentJson = "";
4705
4916
  try {
4706
- currentJson = await readFile13(filePath, "utf-8");
4917
+ currentJson = await readFile14(filePath, "utf-8");
4707
4918
  } catch {
4708
4919
  }
4920
+ if (createOnly && currentJson !== "") continue;
4709
4921
  if (currentJson === newJson) continue;
4710
- await writeFile10(filePath, newJson, "utf-8");
4922
+ await writeFile11(filePath, newJson, "utf-8");
4711
4923
  console.log(` Updated .codebyplan/${name}`);
4712
4924
  anyUpdated = true;
4713
4925
  }
@@ -4717,8 +4929,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4717
4929
  }
4718
4930
  async function readRepoConfig(projectPath) {
4719
4931
  try {
4720
- const raw = await readFile13(
4721
- join14(projectPath, ".codebyplan", "repo.json"),
4932
+ const raw = await readFile14(
4933
+ join15(projectPath, ".codebyplan", "repo.json"),
4722
4934
  "utf-8"
4723
4935
  );
4724
4936
  return JSON.parse(raw);
@@ -4728,8 +4940,8 @@ async function readRepoConfig(projectPath) {
4728
4940
  }
4729
4941
  async function readServerConfig(projectPath) {
4730
4942
  try {
4731
- const raw = await readFile13(
4732
- join14(projectPath, ".codebyplan", "server.json"),
4943
+ const raw = await readFile14(
4944
+ join15(projectPath, ".codebyplan", "server.json"),
4733
4945
  "utf-8"
4734
4946
  );
4735
4947
  return JSON.parse(raw);
@@ -4739,8 +4951,8 @@ async function readServerConfig(projectPath) {
4739
4951
  }
4740
4952
  async function readGitConfig(projectPath) {
4741
4953
  try {
4742
- const raw = await readFile13(
4743
- join14(projectPath, ".codebyplan", "git.json"),
4954
+ const raw = await readFile14(
4955
+ join15(projectPath, ".codebyplan", "git.json"),
4744
4956
  "utf-8"
4745
4957
  );
4746
4958
  return JSON.parse(raw);
@@ -4750,8 +4962,8 @@ async function readGitConfig(projectPath) {
4750
4962
  }
4751
4963
  async function readShipmentConfig(projectPath) {
4752
4964
  try {
4753
- const raw = await readFile13(
4754
- join14(projectPath, ".codebyplan", "shipment.json"),
4965
+ const raw = await readFile14(
4966
+ join15(projectPath, ".codebyplan", "shipment.json"),
4755
4967
  "utf-8"
4756
4968
  );
4757
4969
  return JSON.parse(raw);
@@ -4761,8 +4973,19 @@ async function readShipmentConfig(projectPath) {
4761
4973
  }
4762
4974
  async function readVendorConfig(projectPath) {
4763
4975
  try {
4764
- const raw = await readFile13(
4765
- join14(projectPath, ".codebyplan", "vendor.json"),
4976
+ const raw = await readFile14(
4977
+ join15(projectPath, ".codebyplan", "vendor.json"),
4978
+ "utf-8"
4979
+ );
4980
+ return JSON.parse(raw);
4981
+ } catch {
4982
+ return null;
4983
+ }
4984
+ }
4985
+ async function readE2eConfig(projectPath) {
4986
+ try {
4987
+ const raw = await readFile14(
4988
+ join15(projectPath, ".codebyplan", "e2e.json"),
4766
4989
  "utf-8"
4767
4990
  );
4768
4991
  return JSON.parse(raw);
@@ -4818,14 +5041,14 @@ var init_server_detect = __esm({
4818
5041
  });
4819
5042
 
4820
5043
  // src/lib/port-verify.ts
4821
- import { readFile as readFile14 } from "node:fs/promises";
5044
+ import { readFile as readFile15 } from "node:fs/promises";
4822
5045
  async function verifyPorts(projectPath, portAllocations) {
4823
5046
  const mismatches = [];
4824
5047
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
4825
5048
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
4826
5049
  for (const pkgPath of packageJsonPaths) {
4827
5050
  try {
4828
- const raw = await readFile14(pkgPath, "utf-8");
5051
+ const raw = await readFile15(pkgPath, "utf-8");
4829
5052
  const pkg = JSON.parse(raw);
4830
5053
  const scriptPort = detectPortFromScripts(pkg);
4831
5054
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -4888,7 +5111,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
4888
5111
  }
4889
5112
  let pkg;
4890
5113
  try {
4891
- const raw = await readFile14(`${app.absPath}/package.json`, "utf-8");
5114
+ const raw = await readFile15(`${app.absPath}/package.json`, "utf-8");
4892
5115
  pkg = JSON.parse(raw);
4893
5116
  } catch {
4894
5117
  continue;
@@ -5258,7 +5481,7 @@ var init_hash = __esm({
5258
5481
 
5259
5482
  // src/lib/template-walker.ts
5260
5483
  import * as fs from "node:fs";
5261
- import * as path from "node:path";
5484
+ import * as path2 from "node:path";
5262
5485
  function walkTemplates(templatesDir) {
5263
5486
  const absRoot = fs.realpathSync(templatesDir);
5264
5487
  const visited = /* @__PURE__ */ new Set();
@@ -5271,7 +5494,7 @@ function walkTemplates(templatesDir) {
5271
5494
  visited.add(realDir);
5272
5495
  const entries = fs.readdirSync(absDir, { withFileTypes: true });
5273
5496
  for (const entry of entries) {
5274
- const absPath = path.join(absDir, entry.name);
5497
+ const absPath = path2.join(absDir, entry.name);
5275
5498
  if (entry.isDirectory()) {
5276
5499
  recurse(absPath);
5277
5500
  continue;
@@ -5280,7 +5503,7 @@ function walkTemplates(templatesDir) {
5280
5503
  continue;
5281
5504
  }
5282
5505
  if (entry.name === ".gitkeep") continue;
5283
- const relPosix = path.relative(absRoot, absPath).split(path.sep).join("/");
5506
+ const relPosix = path2.relative(absRoot, absPath).split(path2.sep).join("/");
5284
5507
  if (EXCLUDED_RELATIVE_PATHS.has(relPosix)) {
5285
5508
  continue;
5286
5509
  }
@@ -5308,6 +5531,10 @@ var init_template_walker = __esm({
5308
5531
  "rules/README.md",
5309
5532
  "settings.project.base.json",
5310
5533
  "settings.user.base.json",
5534
+ // .gitignore — managed by ensureManagedGitignoreBlock; never copied into
5535
+ // consuming projects' .claude/ tree (it would overwrite the project root
5536
+ // .gitignore with a stale single-entry file).
5537
+ ".gitignore",
5311
5538
  // CBP-internal hooks — see templates/hooks/README.md "Hooks NOT included and why"
5312
5539
  "hooks/validate-structure.sh",
5313
5540
  "hooks/validate-structure-lib.sh",
@@ -5325,15 +5552,15 @@ var init_template_walker = __esm({
5325
5552
  // src/lib/manifest.ts
5326
5553
  import * as fs2 from "node:fs";
5327
5554
  import * as os from "node:os";
5328
- import * as path2 from "node:path";
5555
+ import * as path3 from "node:path";
5329
5556
  function manifestPath(projectDir) {
5330
- return path2.join(projectDir, ".claude", NEW_MANIFEST_FILENAME);
5557
+ return path3.join(projectDir, ".claude", NEW_MANIFEST_FILENAME);
5331
5558
  }
5332
5559
  function midManifestPath(projectDir) {
5333
- return path2.join(projectDir, ".claude", MID_MANIFEST_FILENAME);
5560
+ return path3.join(projectDir, ".claude", MID_MANIFEST_FILENAME);
5334
5561
  }
5335
5562
  function oldManifestPath(projectDir) {
5336
- return path2.join(projectDir, ".claude", OLD_MANIFEST_FILENAME);
5563
+ return path3.join(projectDir, ".claude", OLD_MANIFEST_FILENAME);
5337
5564
  }
5338
5565
  function readManifest(projectDir) {
5339
5566
  const newFile = manifestPath(projectDir);
@@ -5355,7 +5582,7 @@ function readManifest(projectDir) {
5355
5582
  }
5356
5583
  function writeManifest(projectDir, manifest) {
5357
5584
  const file = manifestPath(projectDir);
5358
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
5585
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
5359
5586
  fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
5360
5587
  const mid = midManifestPath(projectDir);
5361
5588
  if (fs2.existsSync(mid)) {
@@ -5374,16 +5601,16 @@ function defaultManifest() {
5374
5601
  };
5375
5602
  }
5376
5603
  function userManifestPath(userDir) {
5377
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5378
- return path2.join(dir, NEW_MANIFEST_FILENAME);
5604
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5605
+ return path3.join(dir, NEW_MANIFEST_FILENAME);
5379
5606
  }
5380
5607
  function userMidManifestPath(userDir) {
5381
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5382
- return path2.join(dir, MID_MANIFEST_FILENAME);
5608
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5609
+ return path3.join(dir, MID_MANIFEST_FILENAME);
5383
5610
  }
5384
5611
  function userOldManifestPath(userDir) {
5385
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5386
- return path2.join(dir, OLD_MANIFEST_FILENAME);
5612
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5613
+ return path3.join(dir, OLD_MANIFEST_FILENAME);
5387
5614
  }
5388
5615
  function readManifestForScope(scope, arg2) {
5389
5616
  if (scope === "user") {
@@ -5409,7 +5636,7 @@ function readManifestForScope(scope, arg2) {
5409
5636
  function writeManifestForScope(scope, manifest, arg3) {
5410
5637
  if (scope === "user") {
5411
5638
  const file = userManifestPath(arg3);
5412
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
5639
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
5413
5640
  fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
5414
5641
  const mid = userMidManifestPath(arg3);
5415
5642
  if (fs2.existsSync(mid)) {
@@ -5665,14 +5892,14 @@ __export(install_exports, {
5665
5892
  });
5666
5893
  import * as fs3 from "node:fs";
5667
5894
  import * as os2 from "node:os";
5668
- import * as path3 from "node:path";
5895
+ import * as path4 from "node:path";
5669
5896
  import { fileURLToPath } from "node:url";
5670
5897
  function resolveTemplatesDir() {
5671
- const here = path3.dirname(fileURLToPath(import.meta.url));
5898
+ const here = path4.dirname(fileURLToPath(import.meta.url));
5672
5899
  const candidates = [
5673
- path3.resolve(here, "..", "templates"),
5674
- path3.resolve(here, "..", "..", "templates"),
5675
- path3.resolve(here, "..", "..", "..", "templates")
5900
+ path4.resolve(here, "..", "templates"),
5901
+ path4.resolve(here, "..", "..", "templates"),
5902
+ path4.resolve(here, "..", "..", "..", "templates")
5676
5903
  ];
5677
5904
  for (const c of candidates) {
5678
5905
  if (fs3.existsSync(c) && fs3.statSync(c).isDirectory()) {
@@ -5713,14 +5940,14 @@ async function runInstall(opts, deps = {}) {
5713
5940
  const files = walkTemplates(templatesDir);
5714
5941
  const manifestEntries = [];
5715
5942
  for (const f of files) {
5716
- const absDest = path3.join(projectDir, ".claude", f.dest);
5717
- const absSrc = path3.join(templatesDir, f.src);
5943
+ const absDest = path4.join(projectDir, ".claude", f.dest);
5944
+ const absSrc = path4.join(templatesDir, f.src);
5718
5945
  if (opts.dryRun) {
5719
5946
  if (opts.verbose) {
5720
5947
  console.log(`[dry-run] would copy ${f.src} \u2192 .claude/${f.dest}`);
5721
5948
  }
5722
5949
  } else {
5723
- fs3.mkdirSync(path3.dirname(absDest), { recursive: true });
5950
+ fs3.mkdirSync(path4.dirname(absDest), { recursive: true });
5724
5951
  fs3.copyFileSync(absSrc, absDest);
5725
5952
  if (opts.verbose) {
5726
5953
  console.log(`copied ${f.src} \u2192 .claude/${f.dest}`);
@@ -5728,15 +5955,15 @@ async function runInstall(opts, deps = {}) {
5728
5955
  }
5729
5956
  manifestEntries.push({ src: f.src, dest: f.dest, hash: f.hash });
5730
5957
  }
5731
- const hooksJsonPath = path3.join(templatesDir, "hooks", "hooks.json");
5732
- const baseSettingsPath = path3.join(
5958
+ const hooksJsonPath = path4.join(templatesDir, "hooks", "hooks.json");
5959
+ const baseSettingsPath = path4.join(
5733
5960
  templatesDir,
5734
5961
  "settings.project.base.json"
5735
5962
  );
5736
5963
  const hasHooks = fs3.existsSync(hooksJsonPath);
5737
5964
  const hasBase = fs3.existsSync(baseSettingsPath);
5738
5965
  if (hasHooks || hasBase) {
5739
- const settingsPath = path3.join(projectDir, ".claude", "settings.json");
5966
+ const settingsPath = path4.join(projectDir, ".claude", "settings.json");
5740
5967
  const existingSettings = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf8")) : {};
5741
5968
  if (hasBase) {
5742
5969
  const base = JSON.parse(
@@ -5751,7 +5978,7 @@ async function runInstall(opts, deps = {}) {
5751
5978
  mergeHooksIntoSettings(existingSettings, hooksJson);
5752
5979
  }
5753
5980
  if (!opts.dryRun) {
5754
- fs3.mkdirSync(path3.dirname(settingsPath), { recursive: true });
5981
+ fs3.mkdirSync(path4.dirname(settingsPath), { recursive: true });
5755
5982
  fs3.writeFileSync(
5756
5983
  settingsPath,
5757
5984
  JSON.stringify(existingSettings, null, 2) + "\n",
@@ -5759,10 +5986,19 @@ async function runInstall(opts, deps = {}) {
5759
5986
  );
5760
5987
  } else if (opts.verbose) {
5761
5988
  console.log(
5762
- `[dry-run] would merge settings into ${path3.relative(projectDir, settingsPath)}`
5989
+ `[dry-run] would merge settings into ${path4.relative(projectDir, settingsPath)}`
5763
5990
  );
5764
5991
  }
5765
5992
  }
5993
+ const gitignoreAction = await ensureManagedGitignoreBlock(
5994
+ projectDir,
5995
+ opts.dryRun
5996
+ );
5997
+ if (opts.verbose && gitignoreAction !== "unchanged") {
5998
+ console.log(
5999
+ `${opts.dryRun ? "[dry-run] would " : ""}${gitignoreAction} managed .gitignore block in ${path4.relative(projectDir, path4.join(projectDir, ".gitignore"))}`
6000
+ );
6001
+ }
5766
6002
  if (!opts.dryRun) {
5767
6003
  const manifest = defaultManifest();
5768
6004
  manifest.files = manifestEntries;
@@ -5793,9 +6029,9 @@ function runInstallUser(opts, deps) {
5793
6029
  return;
5794
6030
  }
5795
6031
  try {
5796
- const userDir = deps.userDir ?? path3.join(os2.homedir(), ".claude");
5797
- const settingsPath = path3.join(userDir, "settings.json");
5798
- const userBaseSettingsPath = path3.join(
6032
+ const userDir = deps.userDir ?? path4.join(os2.homedir(), ".claude");
6033
+ const settingsPath = path4.join(userDir, "settings.json");
6034
+ const userBaseSettingsPath = path4.join(
5799
6035
  templatesDir,
5800
6036
  "settings.user.base.json"
5801
6037
  );
@@ -5837,7 +6073,7 @@ function runInstallUser(opts, deps) {
5837
6073
  }
5838
6074
  }
5839
6075
  function countHookEntries(templatesDir) {
5840
- const p = path3.join(templatesDir, "hooks", "hooks.json");
6076
+ const p = path4.join(templatesDir, "hooks", "hooks.json");
5841
6077
  if (!fs3.existsSync(p)) return 0;
5842
6078
  try {
5843
6079
  const j = JSON.parse(fs3.readFileSync(p, "utf8"));
@@ -5857,6 +6093,7 @@ var init_install = __esm({
5857
6093
  "src/cli/claude/install.ts"() {
5858
6094
  "use strict";
5859
6095
  init_template_walker();
6096
+ init_gitignore_block();
5860
6097
  init_manifest();
5861
6098
  init_settings_merge();
5862
6099
  init_statusline_config();
@@ -5981,7 +6218,7 @@ __export(update_exports, {
5981
6218
  });
5982
6219
  import * as fs4 from "node:fs";
5983
6220
  import * as os3 from "node:os";
5984
- import * as path4 from "node:path";
6221
+ import * as path5 from "node:path";
5985
6222
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5986
6223
  async function runUpdate(opts, deps = {}) {
5987
6224
  await Promise.resolve();
@@ -6021,9 +6258,9 @@ async function runUpdate(opts, deps = {}) {
6021
6258
  finalManifestEntries.push(e);
6022
6259
  }
6023
6260
  for (const { packaged, absSrc } of plan.overwriteSafe) {
6024
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6261
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6025
6262
  if (!opts.dryRun) {
6026
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6263
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6027
6264
  fs4.copyFileSync(absSrc, absDest);
6028
6265
  if (opts.verbose) console.log(`updated ${packaged.dest}`);
6029
6266
  } else if (opts.verbose) {
@@ -6036,7 +6273,7 @@ async function runUpdate(opts, deps = {}) {
6036
6273
  absSrc,
6037
6274
  onDiskContent
6038
6275
  } of plan.overwriteHandEdited) {
6039
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6276
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6040
6277
  const newContent = fs4.readFileSync(absSrc);
6041
6278
  const showDiff = () => {
6042
6279
  console.log(
@@ -6049,7 +6286,7 @@ async function runUpdate(opts, deps = {}) {
6049
6286
  const answer = await promptOverwrite(packaged.dest, opts, showDiff);
6050
6287
  if (answer === "overwrite") {
6051
6288
  if (!opts.dryRun) {
6052
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6289
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6053
6290
  fs4.copyFileSync(absSrc, absDest);
6054
6291
  }
6055
6292
  finalManifestEntries.push(packaged);
@@ -6065,9 +6302,9 @@ async function runUpdate(opts, deps = {}) {
6065
6302
  for (const { packaged, absSrc } of plan.newOptIn) {
6066
6303
  const answer = await promptOptIn(packaged.dest, opts);
6067
6304
  if (answer === "opt-in") {
6068
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6305
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6069
6306
  if (!opts.dryRun) {
6070
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6307
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6071
6308
  fs4.copyFileSync(absSrc, absDest);
6072
6309
  }
6073
6310
  finalManifestEntries.push(packaged);
@@ -6079,25 +6316,25 @@ async function runUpdate(opts, deps = {}) {
6079
6316
  for (const e of plan.removedFromPackage) {
6080
6317
  const answer = await promptRemove(e.dest, opts);
6081
6318
  if (answer === "remove") {
6082
- const absDest = path4.join(projectDir, ".claude", e.dest);
6319
+ const absDest = path5.join(projectDir, ".claude", e.dest);
6083
6320
  if (!opts.dryRun && fs4.existsSync(absDest)) {
6084
6321
  fs4.rmSync(absDest);
6085
- const claudeDir = path4.join(projectDir, ".claude");
6086
- let cur = path4.dirname(absDest);
6087
- while (cur !== claudeDir && cur !== path4.dirname(cur)) {
6088
- if (path4.dirname(cur) === claudeDir) break;
6322
+ const claudeDir = path5.join(projectDir, ".claude");
6323
+ let cur = path5.dirname(absDest);
6324
+ while (cur !== claudeDir && cur !== path5.dirname(cur)) {
6325
+ if (path5.dirname(cur) === claudeDir) break;
6089
6326
  try {
6090
6327
  fs4.rmdirSync(cur);
6091
6328
  if (opts.verbose)
6092
6329
  console.log(
6093
- `pruned empty dir ${path4.relative(claudeDir, cur)}`
6330
+ `pruned empty dir ${path5.relative(claudeDir, cur)}`
6094
6331
  );
6095
- cur = path4.dirname(cur);
6332
+ cur = path5.dirname(cur);
6096
6333
  } catch (err) {
6097
6334
  const code = err.code;
6098
6335
  if (code !== "ENOTEMPTY" && code !== "ENOENT") {
6099
6336
  console.warn(
6100
- `codebyplan claude: could not prune empty dir ${path4.relative(claudeDir, cur)}: ${err.message}`
6337
+ `codebyplan claude: could not prune empty dir ${path5.relative(claudeDir, cur)}: ${err.message}`
6101
6338
  );
6102
6339
  }
6103
6340
  break;
@@ -6109,16 +6346,16 @@ async function runUpdate(opts, deps = {}) {
6109
6346
  if (opts.verbose) console.log(`kept (untracked) ${e.dest}`);
6110
6347
  }
6111
6348
  }
6112
- const hooksJsonPath = path4.join(templatesDir, "hooks", "hooks.json");
6349
+ const hooksJsonPath = path5.join(templatesDir, "hooks", "hooks.json");
6113
6350
  if (fs4.existsSync(hooksJsonPath)) {
6114
6351
  const hooksJson = JSON.parse(
6115
6352
  fs4.readFileSync(hooksJsonPath, "utf8")
6116
6353
  );
6117
- const settingsPath = path4.join(projectDir, ".claude", "settings.json");
6354
+ const settingsPath = path5.join(projectDir, ".claude", "settings.json");
6118
6355
  const existingSettings = fs4.existsSync(settingsPath) ? JSON.parse(fs4.readFileSync(settingsPath, "utf8")) : {};
6119
6356
  mergeHooksIntoSettings(existingSettings, hooksJson);
6120
6357
  if (!opts.dryRun) {
6121
- fs4.mkdirSync(path4.dirname(settingsPath), { recursive: true });
6358
+ fs4.mkdirSync(path5.dirname(settingsPath), { recursive: true });
6122
6359
  fs4.writeFileSync(
6123
6360
  settingsPath,
6124
6361
  JSON.stringify(existingSettings, null, 2) + "\n",
@@ -6126,6 +6363,15 @@ async function runUpdate(opts, deps = {}) {
6126
6363
  );
6127
6364
  }
6128
6365
  }
6366
+ const gitignoreAction = await ensureManagedGitignoreBlock(
6367
+ projectDir,
6368
+ opts.dryRun
6369
+ );
6370
+ if (opts.verbose && gitignoreAction !== "unchanged") {
6371
+ console.log(
6372
+ `${opts.dryRun ? "[dry-run] would " : ""}${gitignoreAction} managed .gitignore block in ${path5.relative(projectDir, path5.join(projectDir, ".gitignore"))}`
6373
+ );
6374
+ }
6129
6375
  if (!opts.dryRun) {
6130
6376
  const manifest = defaultManifest();
6131
6377
  manifest.files = finalManifestEntries.sort(
@@ -6158,9 +6404,9 @@ function runUpdateUser(opts, deps) {
6158
6404
  return;
6159
6405
  }
6160
6406
  try {
6161
- const userDir = deps.userDir ?? path4.join(os3.homedir(), ".claude");
6162
- const settingsPath = path4.join(userDir, "settings.json");
6163
- const userBaseSettingsPath = path4.join(
6407
+ const userDir = deps.userDir ?? path5.join(os3.homedir(), ".claude");
6408
+ const settingsPath = path5.join(userDir, "settings.json");
6409
+ const userBaseSettingsPath = path5.join(
6164
6410
  templatesDir,
6165
6411
  "settings.user.base.json"
6166
6412
  );
@@ -6222,8 +6468,8 @@ function buildPlan(projectDir, templatesDir, manifest) {
6222
6468
  };
6223
6469
  for (const pkg of packaged) {
6224
6470
  const inManifest = manifestBySrc.get(pkg.src);
6225
- const absDest = path4.join(projectDir, ".claude", pkg.dest);
6226
- const absSrc = path4.join(templatesDir, pkg.src);
6471
+ const absDest = path5.join(projectDir, ".claude", pkg.dest);
6472
+ const absSrc = path5.join(templatesDir, pkg.src);
6227
6473
  if (!inManifest) {
6228
6474
  plan.newOptIn.push({
6229
6475
  packaged: { src: pkg.src, dest: pkg.dest, hash: pkg.hash },
@@ -6259,11 +6505,11 @@ function buildPlan(projectDir, templatesDir, manifest) {
6259
6505
  return plan;
6260
6506
  }
6261
6507
  function resolveTemplatesDirFromInstall() {
6262
- const here = path4.dirname(fileURLToPath2(import.meta.url));
6508
+ const here = path5.dirname(fileURLToPath2(import.meta.url));
6263
6509
  const candidates = [
6264
- path4.resolve(here, "..", "templates"),
6265
- path4.resolve(here, "..", "..", "templates"),
6266
- path4.resolve(here, "..", "..", "..", "templates")
6510
+ path5.resolve(here, "..", "templates"),
6511
+ path5.resolve(here, "..", "..", "templates"),
6512
+ path5.resolve(here, "..", "..", "..", "templates")
6267
6513
  ];
6268
6514
  for (const c of candidates) {
6269
6515
  if (fs4.existsSync(c) && fs4.statSync(c).isDirectory()) {
@@ -6281,6 +6527,7 @@ var init_update = __esm({
6281
6527
  "src/cli/claude/update.ts"() {
6282
6528
  "use strict";
6283
6529
  init_template_walker();
6530
+ init_gitignore_block();
6284
6531
  init_hash();
6285
6532
  init_manifest();
6286
6533
  init_settings_merge();
@@ -6296,7 +6543,7 @@ __export(uninstall_exports, {
6296
6543
  });
6297
6544
  import * as fs5 from "node:fs";
6298
6545
  import * as os4 from "node:os";
6299
- import * as path5 from "node:path";
6546
+ import * as path6 from "node:path";
6300
6547
  async function runUninstall(opts, deps = {}) {
6301
6548
  await Promise.resolve();
6302
6549
  const scope = opts.scope ?? "project";
@@ -6325,7 +6572,7 @@ async function runUninstall(opts, deps = {}) {
6325
6572
  let removed = 0;
6326
6573
  let warnings = 0;
6327
6574
  for (const entry of manifest.files) {
6328
- const abs = path5.join(projectDir, ".claude", entry.dest);
6575
+ const abs = path6.join(projectDir, ".claude", entry.dest);
6329
6576
  if (!fs5.existsSync(abs)) {
6330
6577
  console.warn(
6331
6578
  `codebyplan claude uninstall: ${entry.dest} already absent (skipping).`
@@ -6349,12 +6596,12 @@ async function runUninstall(opts, deps = {}) {
6349
6596
  if (!opts.dryRun) {
6350
6597
  pruneEmptyManagedDirs(projectDir);
6351
6598
  }
6352
- const settingsPath = path5.join(projectDir, ".claude", "settings.json");
6599
+ const settingsPath = path6.join(projectDir, ".claude", "settings.json");
6353
6600
  if (fs5.existsSync(settingsPath)) {
6354
6601
  const settings = JSON.parse(
6355
6602
  fs5.readFileSync(settingsPath, "utf8")
6356
6603
  );
6357
- const baseSettingsPath = templatesDir ? path5.join(templatesDir, "settings.project.base.json") : null;
6604
+ const baseSettingsPath = templatesDir ? path6.join(templatesDir, "settings.project.base.json") : null;
6358
6605
  if (baseSettingsPath && fs5.existsSync(baseSettingsPath)) {
6359
6606
  const base = JSON.parse(
6360
6607
  fs5.readFileSync(baseSettingsPath, "utf8")
@@ -6375,6 +6622,15 @@ async function runUninstall(opts, deps = {}) {
6375
6622
  }
6376
6623
  }
6377
6624
  }
6625
+ const gitignoreAction = await removeManagedGitignoreBlock(
6626
+ projectDir,
6627
+ opts.dryRun
6628
+ );
6629
+ if (opts.verbose) {
6630
+ console.log(
6631
+ gitignoreAction === "removed" ? `${opts.dryRun ? "[dry-run] would remove" : "removed"} managed .gitignore block` : "managed .gitignore block already absent (no change)"
6632
+ );
6633
+ }
6378
6634
  if (!opts.dryRun) {
6379
6635
  const m = manifestPath(projectDir);
6380
6636
  if (fs5.existsSync(m)) fs5.rmSync(m);
@@ -6403,7 +6659,7 @@ function runUninstallUser(opts, deps) {
6403
6659
  }
6404
6660
  }
6405
6661
  try {
6406
- const userDir = deps.userDir ?? path5.join(os4.homedir(), ".claude");
6662
+ const userDir = deps.userDir ?? path6.join(os4.homedir(), ".claude");
6407
6663
  const existingManifest = readManifestForScope("user", userDir);
6408
6664
  if (!existingManifest) {
6409
6665
  console.error(
@@ -6412,12 +6668,12 @@ function runUninstallUser(opts, deps) {
6412
6668
  process.exitCode = 1;
6413
6669
  return;
6414
6670
  }
6415
- const settingsPath = path5.join(userDir, "settings.json");
6671
+ const settingsPath = path6.join(userDir, "settings.json");
6416
6672
  if (fs5.existsSync(settingsPath)) {
6417
6673
  const settings = JSON.parse(
6418
6674
  fs5.readFileSync(settingsPath, "utf8")
6419
6675
  );
6420
- const userBaseSettingsPath = templatesDir != null ? path5.join(templatesDir, "settings.user.base.json") : null;
6676
+ const userBaseSettingsPath = templatesDir != null ? path6.join(templatesDir, "settings.user.base.json") : null;
6421
6677
  if (userBaseSettingsPath && fs5.existsSync(userBaseSettingsPath)) {
6422
6678
  const userBase = JSON.parse(
6423
6679
  fs5.readFileSync(userBaseSettingsPath, "utf8")
@@ -6458,7 +6714,7 @@ function runUninstallUser(opts, deps) {
6458
6714
  function pruneEmptyManagedDirs(projectDir) {
6459
6715
  const managedRoots = ["skills", "agents", "hooks", "rules"];
6460
6716
  for (const root of managedRoots) {
6461
- const abs = path5.join(projectDir, ".claude", root);
6717
+ const abs = path6.join(projectDir, ".claude", root);
6462
6718
  if (!fs5.existsSync(abs)) continue;
6463
6719
  pruneLeafFirst(abs);
6464
6720
  }
@@ -6469,7 +6725,7 @@ function pruneLeafFirst(dir) {
6469
6725
  if (!stat2.isDirectory()) return;
6470
6726
  for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
6471
6727
  if (entry.isDirectory()) {
6472
- pruneLeafFirst(path5.join(dir, entry.name));
6728
+ pruneLeafFirst(path6.join(dir, entry.name));
6473
6729
  }
6474
6730
  }
6475
6731
  const remaining = fs5.readdirSync(dir);
@@ -6481,6 +6737,7 @@ var init_uninstall = __esm({
6481
6737
  "src/cli/claude/uninstall.ts"() {
6482
6738
  "use strict";
6483
6739
  init_hash();
6740
+ init_gitignore_block();
6484
6741
  init_manifest();
6485
6742
  init_settings_merge();
6486
6743
  init_install();