codebyplan 1.11.2 → 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.
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.2";
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,35 +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
- await writeFile5(
1199
- join5(codebyplanDir, "e2e.json"),
1336
+ await writeFile6(
1337
+ join6(codebyplanDir, "e2e.json"),
1200
1338
  JSON.stringify({}, null, 2) + "\n",
1201
1339
  "utf-8"
1202
1340
  );
1203
- const statuslinePath = join5(codebyplanDir, "statusline.json");
1341
+ const statuslinePath = join6(codebyplanDir, "statusline.json");
1204
1342
  let statuslineExists = false;
1205
1343
  try {
1206
- await readFile5(statuslinePath, "utf-8");
1344
+ await readFile6(statuslinePath, "utf-8");
1207
1345
  statuslineExists = true;
1208
1346
  } catch {
1209
1347
  }
1210
1348
  if (!statuslineExists) {
1211
- await writeFile5(
1349
+ await writeFile6(
1212
1350
  statuslinePath,
1213
1351
  JSON.stringify(STATUSLINE_DEFAULTS, null, 2) + "\n",
1214
1352
  "utf-8"
@@ -1220,33 +1358,9 @@ async function writeCodebyplanDirectory(projectPath, selectedRepo, deviceId) {
1220
1358
  ` repo.json, server.json, git.json, shipment.json, vendor.json, e2e.json, statusline.json`
1221
1359
  );
1222
1360
  console.log(` device.local.json (gitignored)`);
1223
- const gitignorePath = join5(projectPath, ".gitignore");
1224
- const gitignoreEntry = ".codebyplan/device.local.json";
1225
- const statuslineLocalEntry = ".codebyplan/statusline.local.json";
1226
- try {
1227
- const existing = await readFile5(gitignorePath, "utf-8");
1228
- const lines = existing.split("\n");
1229
- let content = existing;
1230
- if (!lines.some((l) => l.trimEnd() === gitignoreEntry)) {
1231
- content = (content.endsWith("\n") ? content : content + "\n") + gitignoreEntry + "\n";
1232
- console.log(` Added '${gitignoreEntry}' to .gitignore`);
1233
- }
1234
- if (!lines.some((l) => l.trimEnd() === statuslineLocalEntry)) {
1235
- content = (content.endsWith("\n") ? content : content + "\n") + statuslineLocalEntry + "\n";
1236
- console.log(` Added '${statuslineLocalEntry}' to .gitignore`);
1237
- }
1238
- if (content !== existing) {
1239
- await writeFile5(gitignorePath, content, "utf-8");
1240
- }
1241
- } catch {
1242
- await writeFile5(
1243
- gitignorePath,
1244
- gitignoreEntry + "\n" + statuslineLocalEntry + "\n",
1245
- "utf-8"
1246
- );
1247
- console.log(
1248
- ` Created .gitignore with '${gitignoreEntry}' and '${statuslineLocalEntry}'`
1249
- );
1361
+ const gitignoreAction = await ensureManagedGitignoreBlock(projectPath);
1362
+ if (gitignoreAction !== "unchanged") {
1363
+ console.log(" Updated .gitignore (codebyplan managed block)");
1250
1364
  }
1251
1365
  }
1252
1366
  async function runSetup() {
@@ -1377,6 +1491,7 @@ async function runSetup() {
1377
1491
  var init_setup = __esm({
1378
1492
  "src/cli/setup.ts"() {
1379
1493
  "use strict";
1494
+ init_gitignore_block();
1380
1495
  init_local_config();
1381
1496
  init_statusline_config();
1382
1497
  init_resolve_worktree();
@@ -1387,21 +1502,21 @@ var init_setup = __esm({
1387
1502
  });
1388
1503
 
1389
1504
  // src/lib/flags.ts
1390
- import { readFile as readFile6 } from "node:fs/promises";
1391
- 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";
1392
1507
  async function findCodebyplanConfig(startDir, maxDepth = 20) {
1393
1508
  let cursor = resolve(startDir);
1394
1509
  for (let depth = 0; depth < maxDepth; depth++) {
1395
- const sentinelPath2 = join6(cursor, ".codebyplan", "repo.json");
1510
+ const sentinelPath2 = join7(cursor, ".codebyplan", "repo.json");
1396
1511
  try {
1397
- const raw = await readFile6(sentinelPath2, "utf-8");
1512
+ const raw = await readFile7(sentinelPath2, "utf-8");
1398
1513
  const parsed = JSON.parse(raw);
1399
1514
  return { path: sentinelPath2, contents: parsed };
1400
1515
  } catch {
1401
1516
  }
1402
- const legacyPath = join6(cursor, ".codebyplan.json");
1517
+ const legacyPath = join7(cursor, ".codebyplan.json");
1403
1518
  try {
1404
- const raw = await readFile6(legacyPath, "utf-8");
1519
+ const raw = await readFile7(legacyPath, "utf-8");
1405
1520
  const parsed = JSON.parse(raw);
1406
1521
  return { path: legacyPath, contents: parsed };
1407
1522
  } catch {
@@ -1628,15 +1743,15 @@ var upgrade_auth_exports = {};
1628
1743
  __export(upgrade_auth_exports, {
1629
1744
  runUpgradeAuth: () => runUpgradeAuth
1630
1745
  });
1631
- import { readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
1746
+ import { readFile as readFile8, writeFile as writeFile7 } from "node:fs/promises";
1632
1747
  import { homedir as homedir3 } from "node:os";
1633
- import { join as join7 } from "node:path";
1748
+ import { join as join8 } from "node:path";
1634
1749
  function configPaths() {
1635
- return [join7(homedir3(), ".claude.json"), join7(process.cwd(), ".mcp.json")];
1750
+ return [join8(homedir3(), ".claude.json"), join8(process.cwd(), ".mcp.json")];
1636
1751
  }
1637
- async function readConfig2(path6) {
1752
+ async function readConfig2(path7) {
1638
1753
  try {
1639
- const raw = await readFile7(path6, "utf-8");
1754
+ const raw = await readFile8(path7, "utf-8");
1640
1755
  const parsed = JSON.parse(raw);
1641
1756
  if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1642
1757
  return parsed;
@@ -1650,14 +1765,14 @@ function entryHasLegacyApiKey(entry) {
1650
1765
  if (!entry || !entry.headers) return false;
1651
1766
  return "x-api-key" in entry.headers;
1652
1767
  }
1653
- async function rewriteConfig(path6, config, newUrl) {
1768
+ async function rewriteConfig(path7, config, newUrl) {
1654
1769
  const servers = config.mcpServers;
1655
1770
  if (!servers) return false;
1656
1771
  const entry = servers.codebyplan;
1657
1772
  if (!entry) return false;
1658
1773
  if (!entryHasLegacyApiKey(entry) && entry.url === newUrl) return false;
1659
1774
  servers.codebyplan = { url: newUrl };
1660
- await writeFile6(path6, JSON.stringify(config, null, 2) + "\n", "utf-8");
1775
+ await writeFile7(path7, JSON.stringify(config, null, 2) + "\n", "utf-8");
1661
1776
  return true;
1662
1777
  }
1663
1778
  async function runUpgradeAuth() {
@@ -1665,12 +1780,12 @@ async function runUpgradeAuth() {
1665
1780
  await runLogin();
1666
1781
  const newUrl = mcpEndpoint();
1667
1782
  let migrated = 0;
1668
- for (const path6 of configPaths()) {
1669
- const config = await readConfig2(path6);
1783
+ for (const path7 of configPaths()) {
1784
+ const config = await readConfig2(path7);
1670
1785
  if (!config) continue;
1671
- const changed = await rewriteConfig(path6, config, newUrl);
1786
+ const changed = await rewriteConfig(path7, config, newUrl);
1672
1787
  if (changed) {
1673
- console.log(` Updated ${path6}`);
1788
+ console.log(` Updated ${path7}`);
1674
1789
  migrated++;
1675
1790
  }
1676
1791
  }
@@ -1738,8 +1853,8 @@ var init_confirm = __esm({
1738
1853
  });
1739
1854
 
1740
1855
  // src/lib/tech-detect.ts
1741
- import { readFile as readFile8, access, readdir } from "node:fs/promises";
1742
- 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";
1743
1858
  async function fileExists(filePath) {
1744
1859
  try {
1745
1860
  await access(filePath);
@@ -1752,8 +1867,8 @@ async function discoverMonorepoApps(projectPath) {
1752
1867
  const apps = [];
1753
1868
  const patterns = [];
1754
1869
  try {
1755
- const raw = await readFile8(
1756
- join8(projectPath, "pnpm-workspace.yaml"),
1870
+ const raw = await readFile9(
1871
+ join9(projectPath, "pnpm-workspace.yaml"),
1757
1872
  "utf-8"
1758
1873
  );
1759
1874
  const matches = raw.match(/^\s*-\s*['"]?([^'"#\n]+)['"]?/gm);
@@ -1767,7 +1882,7 @@ async function discoverMonorepoApps(projectPath) {
1767
1882
  }
1768
1883
  if (patterns.length === 0) {
1769
1884
  try {
1770
- const raw = await readFile8(join8(projectPath, "package.json"), "utf-8");
1885
+ const raw = await readFile9(join9(projectPath, "package.json"), "utf-8");
1771
1886
  const pkg = JSON.parse(raw);
1772
1887
  const ws = Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
1773
1888
  if (ws) patterns.push(...ws);
@@ -1777,14 +1892,14 @@ async function discoverMonorepoApps(projectPath) {
1777
1892
  for (const pattern of patterns) {
1778
1893
  if (pattern.endsWith("/*")) {
1779
1894
  const dir = pattern.slice(0, -2);
1780
- const absDir = join8(projectPath, dir);
1895
+ const absDir = join9(projectPath, dir);
1781
1896
  try {
1782
1897
  const entries = await readdir(absDir, { withFileTypes: true });
1783
1898
  for (const entry of entries) {
1784
1899
  if (entry.isDirectory()) {
1785
- const relPath = join8(dir, entry.name);
1786
- const absPath = join8(absDir, entry.name);
1787
- 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"))) {
1788
1903
  apps.push({ name: entry.name, path: relPath, absPath });
1789
1904
  }
1790
1905
  }
@@ -1803,7 +1918,7 @@ async function hasJsxFile(dir, depth = 0) {
1803
1918
  const name = entry.name;
1804
1919
  if (entry.isDirectory()) {
1805
1920
  if (SKIP_DIRS.has(name) || JSX_SKIP_DIRS.has(name)) continue;
1806
- if (await hasJsxFile(join8(dir, name), depth + 1)) return true;
1921
+ if (await hasJsxFile(join9(dir, name), depth + 1)) return true;
1807
1922
  } else if (entry.isFile()) {
1808
1923
  if (JSX_TEST_PATTERN.test(name)) continue;
1809
1924
  if (name.endsWith(".tsx") || name.endsWith(".jsx")) return true;
@@ -1822,7 +1937,7 @@ async function hasJsxFile(dir, depth = 0) {
1822
1937
  async function detectCapabilities(dirPath, pkgJson) {
1823
1938
  const caps = /* @__PURE__ */ new Set();
1824
1939
  for (const sub of JSX_SCAN_DIRS) {
1825
- if (await hasJsxFile(join8(dirPath, sub))) {
1940
+ if (await hasJsxFile(join9(dirPath, sub))) {
1826
1941
  caps.add("jsx");
1827
1942
  break;
1828
1943
  }
@@ -1844,7 +1959,7 @@ async function detectCapabilities(dirPath, pkgJson) {
1844
1959
  }
1845
1960
  }
1846
1961
  }
1847
- 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"))) {
1848
1963
  caps.add("node-server");
1849
1964
  }
1850
1965
  if (pkgJson && pkgJson.bin) {
@@ -1860,7 +1975,7 @@ async function detectFromDirectory(dirPath) {
1860
1975
  const seen = /* @__PURE__ */ new Map();
1861
1976
  let pkgJson = null;
1862
1977
  try {
1863
- const raw = await readFile8(join8(dirPath, "package.json"), "utf-8");
1978
+ const raw = await readFile9(join9(dirPath, "package.json"), "utf-8");
1864
1979
  pkgJson = JSON.parse(raw);
1865
1980
  const allDeps = {
1866
1981
  ...pkgJson.dependencies ?? {},
@@ -1892,7 +2007,7 @@ async function detectFromDirectory(dirPath) {
1892
2007
  }
1893
2008
  for (const { file, rule } of CONFIG_FILE_MAP) {
1894
2009
  const key = rule.name.toLowerCase();
1895
- if (!seen.has(key) && await fileExists(join8(dirPath, file))) {
2010
+ if (!seen.has(key) && await fileExists(join9(dirPath, file))) {
1896
2011
  seen.set(key, { name: rule.name, category: rule.category });
1897
2012
  }
1898
2013
  }
@@ -2070,7 +2185,7 @@ function categorizeDependency(depName) {
2070
2185
  async function findPackageJsonFiles(dir, projectPath, depth = 0) {
2071
2186
  if (depth > 4) return [];
2072
2187
  const results = [];
2073
- const pkgPath = join8(dir, "package.json");
2188
+ const pkgPath = join9(dir, "package.json");
2074
2189
  if (await fileExists(pkgPath)) {
2075
2190
  results.push(pkgPath);
2076
2191
  }
@@ -2079,7 +2194,7 @@ async function findPackageJsonFiles(dir, projectPath, depth = 0) {
2079
2194
  for (const entry of entries) {
2080
2195
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name)) continue;
2081
2196
  const subResults = await findPackageJsonFiles(
2082
- join8(dir, entry.name),
2197
+ join9(dir, entry.name),
2083
2198
  projectPath,
2084
2199
  depth + 1
2085
2200
  );
@@ -2094,7 +2209,7 @@ async function scanAllDependencies(projectPath) {
2094
2209
  const dependencies = [];
2095
2210
  for (const pkgPath of packageJsonPaths) {
2096
2211
  try {
2097
- const raw = await readFile8(pkgPath, "utf-8");
2212
+ const raw = await readFile9(pkgPath, "utf-8");
2098
2213
  const pkg = JSON.parse(raw);
2099
2214
  const sourcePath = relative(projectPath, pkgPath);
2100
2215
  const depSections = [
@@ -2718,8 +2833,8 @@ __export(eslint_exports, {
2718
2833
  eslintInit: () => eslintInit,
2719
2834
  runEslint: () => runEslint
2720
2835
  });
2721
- import { readFile as readFile9, writeFile as writeFile7, access as access2, readdir as readdir2 } from "node:fs/promises";
2722
- 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";
2723
2838
  async function fileExists2(filePath) {
2724
2839
  try {
2725
2840
  await access2(filePath);
@@ -2730,7 +2845,7 @@ async function fileExists2(filePath) {
2730
2845
  }
2731
2846
  async function autoDetectIgnorePatterns(absPath) {
2732
2847
  const patterns = [];
2733
- if (await fileExists2(join9(absPath, "esbuild.js"))) {
2848
+ if (await fileExists2(join10(absPath, "esbuild.js"))) {
2734
2849
  patterns.push("esbuild.js");
2735
2850
  }
2736
2851
  let entries = [];
@@ -2750,19 +2865,19 @@ async function autoDetectIgnorePatterns(absPath) {
2750
2865
  }
2751
2866
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2752
2867
  const candidate = `vitest.config.${ext}`;
2753
- if (await fileExists2(join9(absPath, candidate))) {
2868
+ if (await fileExists2(join10(absPath, candidate))) {
2754
2869
  patterns.push(candidate);
2755
2870
  break;
2756
2871
  }
2757
2872
  }
2758
2873
  for (const ext of ["ts", "mts", "js", "mjs"]) {
2759
2874
  const candidate = `vite.config.${ext}`;
2760
- if (await fileExists2(join9(absPath, candidate))) {
2875
+ if (await fileExists2(join10(absPath, candidate))) {
2761
2876
  patterns.push(candidate);
2762
2877
  break;
2763
2878
  }
2764
2879
  }
2765
- if (await fileExists2(join9(absPath, "tauri.conf.json"))) {
2880
+ if (await fileExists2(join10(absPath, "tauri.conf.json"))) {
2766
2881
  patterns.push("src-tauri/**");
2767
2882
  patterns.push("**/*.d.ts");
2768
2883
  }
@@ -2770,14 +2885,14 @@ async function autoDetectIgnorePatterns(absPath) {
2770
2885
  }
2771
2886
  function detectPackageManager(projectPath) {
2772
2887
  return (async () => {
2773
- if (await fileExists2(join9(projectPath, "pnpm-lock.yaml"))) return "pnpm";
2774
- 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";
2775
2890
  return "npm";
2776
2891
  })();
2777
2892
  }
2778
2893
  async function getInstalledDeps(pkgJsonPath) {
2779
2894
  try {
2780
- const raw = await readFile9(pkgJsonPath, "utf-8");
2895
+ const raw = await readFile10(pkgJsonPath, "utf-8");
2781
2896
  const pkg = JSON.parse(raw);
2782
2897
  const all = /* @__PURE__ */ new Set();
2783
2898
  for (const name of Object.keys(pkg.dependencies ?? {})) all.add(name);
@@ -2890,7 +3005,7 @@ async function eslintInit(repoId, projectPath) {
2890
3005
  ignorePatterns: detectedIgnores
2891
3006
  });
2892
3007
  const hash = hashConfig(content);
2893
- const configPath = join9(target.absPath, "eslint.config.mjs");
3008
+ const configPath = join10(target.absPath, "eslint.config.mjs");
2894
3009
  configsToWrite.push({
2895
3010
  target,
2896
3011
  presets,
@@ -2912,11 +3027,11 @@ async function eslintInit(repoId, projectPath) {
2912
3027
  return;
2913
3028
  }
2914
3029
  const pm = await detectPackageManager(projectPath);
2915
- const rootPkgJsonPath = join9(projectPath, "package.json");
3030
+ const rootPkgJsonPath = join10(projectPath, "package.json");
2916
3031
  const installed = await getInstalledDeps(rootPkgJsonPath);
2917
3032
  if (isMonorepo) {
2918
3033
  for (const { target } of configsToWrite) {
2919
- const appPkgJson = join9(target.absPath, "package.json");
3034
+ const appPkgJson = join10(target.absPath, "package.json");
2920
3035
  const appDeps = await getInstalledDeps(appPkgJson);
2921
3036
  for (const dep of appDeps) {
2922
3037
  installed.add(dep);
@@ -2968,7 +3083,7 @@ async function eslintInit(repoId, projectPath) {
2968
3083
  } of configsToWrite) {
2969
3084
  if (await fileExists2(configPath)) {
2970
3085
  try {
2971
- const existing = await readFile9(configPath, "utf-8");
3086
+ const existing = await readFile10(configPath, "utf-8");
2972
3087
  const existingHash = hashConfig(existing);
2973
3088
  if (existingHash === hash) {
2974
3089
  console.log(
@@ -2988,7 +3103,7 @@ async function eslintInit(repoId, projectPath) {
2988
3103
  }
2989
3104
  }
2990
3105
  try {
2991
- await writeFile7(configPath, content, "utf-8");
3106
+ await writeFile8(configPath, content, "utf-8");
2992
3107
  } catch (err) {
2993
3108
  console.error(
2994
3109
  ` ${target.name}: Failed to write config: ${err instanceof Error ? err.message : String(err)}`
@@ -3292,12 +3407,48 @@ var init_sync_approvals = __esm({
3292
3407
  // src/cli/round.ts
3293
3408
  var round_exports = {};
3294
3409
  __export(round_exports, {
3410
+ RETRY_DELAY_MS: () => RETRY_DELAY_MS,
3411
+ fetchRoundsWithRetry: () => fetchRoundsWithRetry,
3412
+ isTransientMcpError: () => isTransientMcpError,
3295
3413
  runRoundCommand: () => runRoundCommand,
3296
- runRoundSyncApprovals: () => runRoundSyncApprovals
3414
+ runRoundSyncApprovals: () => runRoundSyncApprovals,
3415
+ setRetryDelayMs: () => setRetryDelayMs
3297
3416
  });
3298
3417
  import { access as access3 } from "node:fs/promises";
3299
- import { join as join10 } from "node:path";
3418
+ import { join as join11 } from "node:path";
3300
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
+ }
3301
3452
  async function runRoundCommand(args) {
3302
3453
  const subcommand = args[0];
3303
3454
  if (subcommand === "sync-approvals") {
@@ -3359,6 +3510,7 @@ async function runRoundSyncApprovals(args) {
3359
3510
  );
3360
3511
  process.exit(1);
3361
3512
  }
3513
+ let skipReason = null;
3362
3514
  let stdoutPayload = null;
3363
3515
  try {
3364
3516
  let callerWorktreeId = flags["worktree-id"];
@@ -3370,84 +3522,95 @@ async function runRoundSyncApprovals(args) {
3370
3522
  // Walk up to the directory containing .codebyplan/ or .codebyplan.json
3371
3523
  found.path.replace(/\/.codebyplan(\.json|\/repo\.json)$/, "")
3372
3524
  ) : process.cwd();
3373
- const rounds = await mcpCall("get_rounds", {
3374
- task_id: taskId
3375
- });
3376
- const currentRound = rounds.find((r) => r.id === roundId);
3377
- if (!currentRound) {
3378
- throw new Error(`Round ${roundId} not found for task ${taskId}`);
3379
- }
3380
- const taskResponse = await apiGet(`/tasks/${taskId}`);
3381
- const currentTask = taskResponse.data;
3382
- let gitStatusOutput = "";
3525
+ let rounds;
3383
3526
  try {
3384
- gitStatusOutput = execSync2("git status --short --porcelain -z", {
3385
- cwd: repoRoot,
3386
- encoding: "utf-8"
3387
- });
3388
- } catch {
3389
- process.stderr.write(
3390
- "sync-approvals: git status failed; proceeding with empty diff\n"
3391
- );
3392
- }
3393
- const hookPath = join10(
3394
- repoRoot,
3395
- ".claude",
3396
- "hooks",
3397
- "lint-format-on-edit.sh"
3398
- );
3399
- let lintFormatHookExists = false;
3400
- try {
3401
- await access3(hookPath);
3402
- lintFormatHookExists = true;
3403
- } 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
+ }
3404
3536
  }
3405
- const result = runSyncApprovals({
3406
- currentRound,
3407
- currentTask,
3408
- gitStatusOutput,
3409
- lintFormatHookExists
3410
- });
3411
- if (dryRun) {
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
- merged_files_changed: result.merged_files_changed
3419
- },
3420
- null,
3421
- 2
3422
- );
3423
- } else {
3424
- const roundArgs = {
3425
- round_id: roundId,
3426
- files_changed: result.merged_files_changed
3427
- };
3428
- if (callerWorktreeId) {
3429
- 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}`);
3430
3541
  }
3431
- await mcpCall("update_round", roundArgs);
3432
- const taskArgs = {
3433
- task_id: taskId,
3434
- files_changed: result.merged_files_changed,
3435
- app_file_approval_by_user: false
3436
- };
3437
- if (callerWorktreeId) {
3438
- 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
+ );
3439
3554
  }
3440
- await mcpCall("update_task", taskArgs);
3441
- stdoutPayload = JSON.stringify(
3442
- {
3443
- added: result.added,
3444
- stale_marked: result.stale_marked,
3445
- reactivated: result.reactivated,
3446
- total_files: result.total_files
3447
- },
3448
- null,
3449
- 2
3555
+ const hookPath = join11(
3556
+ repoRoot,
3557
+ ".claude",
3558
+ "hooks",
3559
+ "lint-format-on-edit.sh"
3450
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
+ }
3451
3614
  }
3452
3615
  } catch (err) {
3453
3616
  process.stderr.write(
@@ -3456,6 +3619,13 @@ async function runRoundSyncApprovals(args) {
3456
3619
  );
3457
3620
  process.exit(1);
3458
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
+ }
3459
3629
  if (stdoutPayload === null) {
3460
3630
  process.stderr.write("sync-approvals: internal error \u2014 payload not set\n");
3461
3631
  process.exit(1);
@@ -3499,6 +3669,7 @@ async function autoResolveWorktreeId() {
3499
3669
  return void 0;
3500
3670
  }
3501
3671
  }
3672
+ var RETRY_DELAY_MS;
3502
3673
  var init_round = __esm({
3503
3674
  "src/cli/round.ts"() {
3504
3675
  "use strict";
@@ -3508,12 +3679,13 @@ var init_round = __esm({
3508
3679
  init_resolve_worktree();
3509
3680
  init_local_config();
3510
3681
  init_sync_approvals();
3682
+ RETRY_DELAY_MS = 1e3;
3511
3683
  }
3512
3684
  });
3513
3685
 
3514
3686
  // src/lib/migrate-branch-model.ts
3515
- import { readFile as readFile10, writeFile as writeFile8 } from "node:fs/promises";
3516
- 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";
3517
3689
  import { execSync as execSync3 } from "node:child_process";
3518
3690
  function assertValidBranchName(branch) {
3519
3691
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3523,7 +3695,7 @@ function assertValidBranchName(branch) {
3523
3695
  }
3524
3696
  }
3525
3697
  async function readJsonFile(filePath) {
3526
- const raw = await readFile10(filePath, "utf-8");
3698
+ const raw = await readFile11(filePath, "utf-8");
3527
3699
  const parsed = JSON.parse(raw);
3528
3700
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3529
3701
  throw new Error(`${filePath} does not contain a JSON object`);
@@ -3592,12 +3764,12 @@ async function runBranchMigration(opts) {
3592
3764
  if (found) {
3593
3765
  if (found.path.endsWith("/repo.json")) {
3594
3766
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3595
- configPath = join11(dir, "git.json");
3767
+ configPath = join12(dir, "git.json");
3596
3768
  } else {
3597
3769
  configPath = found.path;
3598
3770
  }
3599
3771
  } else {
3600
- configPath = join11(cwd, ".codebyplan", "git.json");
3772
+ configPath = join12(cwd, ".codebyplan", "git.json");
3601
3773
  }
3602
3774
  let fileRaw;
3603
3775
  let fileParsed;
@@ -3671,7 +3843,7 @@ async function runBranchMigration(opts) {
3671
3843
  const updatedParsed = { ...fileParsed, branch_config: after };
3672
3844
  const newJson = JSON.stringify(updatedParsed, null, 2) + "\n";
3673
3845
  if (newJson !== fileRaw) {
3674
- await writeFile8(configPath, newJson, "utf-8");
3846
+ await writeFile9(configPath, newJson, "utf-8");
3675
3847
  }
3676
3848
  }
3677
3849
  return {
@@ -3777,8 +3949,8 @@ var init_branch = __esm({
3777
3949
  });
3778
3950
 
3779
3951
  // src/lib/ship.ts
3780
- import { readFile as readFile11 } from "node:fs/promises";
3781
- import { join as join12 } from "node:path";
3952
+ import { readFile as readFile12 } from "node:fs/promises";
3953
+ import { join as join13 } from "node:path";
3782
3954
  import { execSync as execSync4, spawnSync as spawnSync2 } from "node:child_process";
3783
3955
  function assertValidBranchName2(branch, label) {
3784
3956
  if (!/^[a-zA-Z0-9/_.-]+$/.test(branch)) {
@@ -3793,15 +3965,15 @@ async function readBaseBranch(cwd) {
3793
3965
  if (found) {
3794
3966
  if (found.path.endsWith("/repo.json")) {
3795
3967
  const dir = found.path.slice(0, found.path.lastIndexOf("/"));
3796
- gitJsonPath = join12(dir, "git.json");
3968
+ gitJsonPath = join13(dir, "git.json");
3797
3969
  } else {
3798
3970
  gitJsonPath = found.path;
3799
3971
  }
3800
3972
  } else {
3801
- gitJsonPath = join12(cwd, ".codebyplan", "git.json");
3973
+ gitJsonPath = join13(cwd, ".codebyplan", "git.json");
3802
3974
  }
3803
3975
  try {
3804
- const raw = await readFile11(gitJsonPath, "utf-8");
3976
+ const raw = await readFile12(gitJsonPath, "utf-8");
3805
3977
  const parsed = JSON.parse(raw);
3806
3978
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3807
3979
  return "main";
@@ -4333,19 +4505,19 @@ var init_resolve_worktree2 = __esm({
4333
4505
  });
4334
4506
 
4335
4507
  // src/lib/migrate-local-config.ts
4336
- import { mkdir as mkdir5, readFile as readFile12, unlink as unlink2, writeFile as writeFile9 } from "node:fs/promises";
4337
- 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";
4338
4510
  function legacySharedPath(projectPath) {
4339
- return join13(projectPath, ".codebyplan.json");
4511
+ return join14(projectPath, ".codebyplan.json");
4340
4512
  }
4341
4513
  function legacyLocalPath(projectPath) {
4342
- return join13(projectPath, ".codebyplan.local.json");
4514
+ return join14(projectPath, ".codebyplan.local.json");
4343
4515
  }
4344
4516
  function newDirPath(projectPath) {
4345
- return join13(projectPath, ".codebyplan");
4517
+ return join14(projectPath, ".codebyplan");
4346
4518
  }
4347
4519
  function sentinelPath(projectPath) {
4348
- return join13(projectPath, ".codebyplan", "repo.json");
4520
+ return join14(projectPath, ".codebyplan", "repo.json");
4349
4521
  }
4350
4522
  async function statSafe(p) {
4351
4523
  const { stat: stat2 } = await import("node:fs/promises");
@@ -4384,7 +4556,7 @@ async function runLocalMigration(projectPath) {
4384
4556
  }
4385
4557
  let legacyRaw;
4386
4558
  try {
4387
- legacyRaw = await readFile12(legacySharedPath(projectPath), "utf-8");
4559
+ legacyRaw = await readFile13(legacySharedPath(projectPath), "utf-8");
4388
4560
  } catch {
4389
4561
  return {
4390
4562
  migrated: true,
@@ -4411,7 +4583,7 @@ async function runLocalMigration(projectPath) {
4411
4583
  let deviceId;
4412
4584
  let deviceWrittenByHelper = false;
4413
4585
  try {
4414
- const localRaw = await readFile12(legacyLocalPath(projectPath), "utf-8");
4586
+ const localRaw = await readFile13(legacyLocalPath(projectPath), "utf-8");
4415
4587
  const localParsed = JSON.parse(localRaw);
4416
4588
  if (typeof localParsed.device_id === "string") {
4417
4589
  deviceId = localParsed.device_id;
@@ -4438,8 +4610,8 @@ async function runLocalMigration(projectPath) {
4438
4610
  if ("repo_id" in cfg) repoJson.repo_id = cfg.repo_id;
4439
4611
  if ("organization_id" in cfg) repoJson.organization_id = cfg.organization_id;
4440
4612
  if ("project_id" in cfg) repoJson.project_id = cfg.project_id;
4441
- await writeFile9(
4442
- join13(projectPath, ".codebyplan", "repo.json"),
4613
+ await writeFile10(
4614
+ join14(projectPath, ".codebyplan", "repo.json"),
4443
4615
  JSON.stringify(repoJson, null, 2) + "\n",
4444
4616
  "utf-8"
4445
4617
  );
@@ -4451,8 +4623,8 @@ async function runLocalMigration(projectPath) {
4451
4623
  serverJson.auto_push_enabled = cfg.auto_push_enabled;
4452
4624
  if ("port_allocations" in cfg)
4453
4625
  serverJson.port_allocations = cfg.port_allocations;
4454
- await writeFile9(
4455
- join13(projectPath, ".codebyplan", "server.json"),
4626
+ await writeFile10(
4627
+ join14(projectPath, ".codebyplan", "server.json"),
4456
4628
  JSON.stringify(serverJson, null, 2) + "\n",
4457
4629
  "utf-8"
4458
4630
  );
@@ -4460,37 +4632,37 @@ async function runLocalMigration(projectPath) {
4460
4632
  const gitJson = {};
4461
4633
  if ("git_branch" in cfg) gitJson.git_branch = cfg.git_branch;
4462
4634
  if ("branch_config" in cfg) gitJson.branch_config = cfg.branch_config;
4463
- await writeFile9(
4464
- join13(projectPath, ".codebyplan", "git.json"),
4635
+ await writeFile10(
4636
+ join14(projectPath, ".codebyplan", "git.json"),
4465
4637
  JSON.stringify(gitJson, null, 2) + "\n",
4466
4638
  "utf-8"
4467
4639
  );
4468
4640
  filesChanged.push(".codebyplan/git.json");
4469
4641
  const shipmentJson = {};
4470
4642
  if ("shipment" in cfg) shipmentJson.shipment = cfg.shipment;
4471
- await writeFile9(
4472
- join13(projectPath, ".codebyplan", "shipment.json"),
4643
+ await writeFile10(
4644
+ join14(projectPath, ".codebyplan", "shipment.json"),
4473
4645
  JSON.stringify(shipmentJson, null, 2) + "\n",
4474
4646
  "utf-8"
4475
4647
  );
4476
4648
  filesChanged.push(".codebyplan/shipment.json");
4477
4649
  const vendorJson = {};
4478
- await writeFile9(
4479
- join13(projectPath, ".codebyplan", "vendor.json"),
4650
+ await writeFile10(
4651
+ join14(projectPath, ".codebyplan", "vendor.json"),
4480
4652
  JSON.stringify(vendorJson, null, 2) + "\n",
4481
4653
  "utf-8"
4482
4654
  );
4483
4655
  filesChanged.push(".codebyplan/vendor.json");
4484
4656
  const e2eJson = {};
4485
- await writeFile9(
4486
- join13(projectPath, ".codebyplan", "e2e.json"),
4657
+ await writeFile10(
4658
+ join14(projectPath, ".codebyplan", "e2e.json"),
4487
4659
  JSON.stringify(e2eJson, null, 2) + "\n",
4488
4660
  "utf-8"
4489
4661
  );
4490
4662
  filesChanged.push(".codebyplan/e2e.json");
4491
4663
  if (!deviceWrittenByHelper) {
4492
- await writeFile9(
4493
- join13(projectPath, ".codebyplan", "device.local.json"),
4664
+ await writeFile10(
4665
+ join14(projectPath, ".codebyplan", "device.local.json"),
4494
4666
  JSON.stringify({ device_id: deviceId }, null, 2) + "\n",
4495
4667
  "utf-8"
4496
4668
  );
@@ -4502,9 +4674,9 @@ async function runLocalMigration(projectPath) {
4502
4674
  "Migration write incomplete: .codebyplan/repo.json was not persisted. Re-run migration to retry from a clean state."
4503
4675
  );
4504
4676
  }
4505
- const gitignorePath = join13(projectPath, ".gitignore");
4677
+ const gitignorePath = join14(projectPath, ".gitignore");
4506
4678
  try {
4507
- const gitignoreContent = await readFile12(gitignorePath, "utf-8");
4679
+ const gitignoreContent = await readFile13(gitignorePath, "utf-8");
4508
4680
  const legacyLine = ".codebyplan.local.json";
4509
4681
  const newLine = ".codebyplan/device.local.json";
4510
4682
  const hasLegacy = gitignoreContent.split("\n").some((l) => l.trimEnd() === legacyLine);
@@ -4523,7 +4695,7 @@ async function runLocalMigration(projectPath) {
4523
4695
  updated = gitignoreContent;
4524
4696
  }
4525
4697
  if (updated !== gitignoreContent) {
4526
- await writeFile9(gitignorePath, updated, "utf-8");
4698
+ await writeFile10(gitignorePath, updated, "utf-8");
4527
4699
  filesChanged.push(".gitignore");
4528
4700
  }
4529
4701
  } catch {
@@ -4563,8 +4735,8 @@ __export(config_exports, {
4563
4735
  readVendorConfig: () => readVendorConfig,
4564
4736
  runConfig: () => runConfig
4565
4737
  });
4566
- import { mkdir as mkdir6, readFile as readFile13, writeFile as writeFile10 } from "node:fs/promises";
4567
- 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";
4568
4740
  async function runConfig() {
4569
4741
  const flags = parseFlags(3);
4570
4742
  const dryRun = hasFlag("dry-run", 3);
@@ -4597,7 +4769,7 @@ async function runConfig() {
4597
4769
  console.log("\n Config complete.\n");
4598
4770
  }
4599
4771
  async function syncConfigToFile(repoId, projectPath, dryRun) {
4600
- const codebyplanDir = join14(projectPath, ".codebyplan");
4772
+ const codebyplanDir = join15(projectPath, ".codebyplan");
4601
4773
  let resolvedWorktreeId;
4602
4774
  try {
4603
4775
  const deviceId = await getOrCreateDeviceId(projectPath);
@@ -4738,16 +4910,16 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4738
4910
  ];
4739
4911
  let anyUpdated = false;
4740
4912
  for (const { name, payload, createOnly } of files) {
4741
- const filePath = join14(codebyplanDir, name);
4913
+ const filePath = join15(codebyplanDir, name);
4742
4914
  const newJson = JSON.stringify(payload, null, 2) + "\n";
4743
4915
  let currentJson = "";
4744
4916
  try {
4745
- currentJson = await readFile13(filePath, "utf-8");
4917
+ currentJson = await readFile14(filePath, "utf-8");
4746
4918
  } catch {
4747
4919
  }
4748
4920
  if (createOnly && currentJson !== "") continue;
4749
4921
  if (currentJson === newJson) continue;
4750
- await writeFile10(filePath, newJson, "utf-8");
4922
+ await writeFile11(filePath, newJson, "utf-8");
4751
4923
  console.log(` Updated .codebyplan/${name}`);
4752
4924
  anyUpdated = true;
4753
4925
  }
@@ -4757,8 +4929,8 @@ async function syncConfigToFile(repoId, projectPath, dryRun) {
4757
4929
  }
4758
4930
  async function readRepoConfig(projectPath) {
4759
4931
  try {
4760
- const raw = await readFile13(
4761
- join14(projectPath, ".codebyplan", "repo.json"),
4932
+ const raw = await readFile14(
4933
+ join15(projectPath, ".codebyplan", "repo.json"),
4762
4934
  "utf-8"
4763
4935
  );
4764
4936
  return JSON.parse(raw);
@@ -4768,8 +4940,8 @@ async function readRepoConfig(projectPath) {
4768
4940
  }
4769
4941
  async function readServerConfig(projectPath) {
4770
4942
  try {
4771
- const raw = await readFile13(
4772
- join14(projectPath, ".codebyplan", "server.json"),
4943
+ const raw = await readFile14(
4944
+ join15(projectPath, ".codebyplan", "server.json"),
4773
4945
  "utf-8"
4774
4946
  );
4775
4947
  return JSON.parse(raw);
@@ -4779,8 +4951,8 @@ async function readServerConfig(projectPath) {
4779
4951
  }
4780
4952
  async function readGitConfig(projectPath) {
4781
4953
  try {
4782
- const raw = await readFile13(
4783
- join14(projectPath, ".codebyplan", "git.json"),
4954
+ const raw = await readFile14(
4955
+ join15(projectPath, ".codebyplan", "git.json"),
4784
4956
  "utf-8"
4785
4957
  );
4786
4958
  return JSON.parse(raw);
@@ -4790,8 +4962,8 @@ async function readGitConfig(projectPath) {
4790
4962
  }
4791
4963
  async function readShipmentConfig(projectPath) {
4792
4964
  try {
4793
- const raw = await readFile13(
4794
- join14(projectPath, ".codebyplan", "shipment.json"),
4965
+ const raw = await readFile14(
4966
+ join15(projectPath, ".codebyplan", "shipment.json"),
4795
4967
  "utf-8"
4796
4968
  );
4797
4969
  return JSON.parse(raw);
@@ -4801,8 +4973,8 @@ async function readShipmentConfig(projectPath) {
4801
4973
  }
4802
4974
  async function readVendorConfig(projectPath) {
4803
4975
  try {
4804
- const raw = await readFile13(
4805
- join14(projectPath, ".codebyplan", "vendor.json"),
4976
+ const raw = await readFile14(
4977
+ join15(projectPath, ".codebyplan", "vendor.json"),
4806
4978
  "utf-8"
4807
4979
  );
4808
4980
  return JSON.parse(raw);
@@ -4812,8 +4984,8 @@ async function readVendorConfig(projectPath) {
4812
4984
  }
4813
4985
  async function readE2eConfig(projectPath) {
4814
4986
  try {
4815
- const raw = await readFile13(
4816
- join14(projectPath, ".codebyplan", "e2e.json"),
4987
+ const raw = await readFile14(
4988
+ join15(projectPath, ".codebyplan", "e2e.json"),
4817
4989
  "utf-8"
4818
4990
  );
4819
4991
  return JSON.parse(raw);
@@ -4869,14 +5041,14 @@ var init_server_detect = __esm({
4869
5041
  });
4870
5042
 
4871
5043
  // src/lib/port-verify.ts
4872
- import { readFile as readFile14 } from "node:fs/promises";
5044
+ import { readFile as readFile15 } from "node:fs/promises";
4873
5045
  async function verifyPorts(projectPath, portAllocations) {
4874
5046
  const mismatches = [];
4875
5047
  const allocatedPorts = new Set(portAllocations.map((a) => a.port));
4876
5048
  const packageJsonPaths = await findPackageJsonFiles(projectPath, projectPath);
4877
5049
  for (const pkgPath of packageJsonPaths) {
4878
5050
  try {
4879
- const raw = await readFile14(pkgPath, "utf-8");
5051
+ const raw = await readFile15(pkgPath, "utf-8");
4880
5052
  const pkg = JSON.parse(raw);
4881
5053
  const scriptPort = detectPortFromScripts(pkg);
4882
5054
  if (scriptPort !== null && !allocatedPorts.has(scriptPort)) {
@@ -4939,7 +5111,7 @@ async function findUnallocatedApps(projectPath, portAllocations) {
4939
5111
  }
4940
5112
  let pkg;
4941
5113
  try {
4942
- const raw = await readFile14(`${app.absPath}/package.json`, "utf-8");
5114
+ const raw = await readFile15(`${app.absPath}/package.json`, "utf-8");
4943
5115
  pkg = JSON.parse(raw);
4944
5116
  } catch {
4945
5117
  continue;
@@ -5309,7 +5481,7 @@ var init_hash = __esm({
5309
5481
 
5310
5482
  // src/lib/template-walker.ts
5311
5483
  import * as fs from "node:fs";
5312
- import * as path from "node:path";
5484
+ import * as path2 from "node:path";
5313
5485
  function walkTemplates(templatesDir) {
5314
5486
  const absRoot = fs.realpathSync(templatesDir);
5315
5487
  const visited = /* @__PURE__ */ new Set();
@@ -5322,7 +5494,7 @@ function walkTemplates(templatesDir) {
5322
5494
  visited.add(realDir);
5323
5495
  const entries = fs.readdirSync(absDir, { withFileTypes: true });
5324
5496
  for (const entry of entries) {
5325
- const absPath = path.join(absDir, entry.name);
5497
+ const absPath = path2.join(absDir, entry.name);
5326
5498
  if (entry.isDirectory()) {
5327
5499
  recurse(absPath);
5328
5500
  continue;
@@ -5331,7 +5503,7 @@ function walkTemplates(templatesDir) {
5331
5503
  continue;
5332
5504
  }
5333
5505
  if (entry.name === ".gitkeep") continue;
5334
- const relPosix = path.relative(absRoot, absPath).split(path.sep).join("/");
5506
+ const relPosix = path2.relative(absRoot, absPath).split(path2.sep).join("/");
5335
5507
  if (EXCLUDED_RELATIVE_PATHS.has(relPosix)) {
5336
5508
  continue;
5337
5509
  }
@@ -5359,6 +5531,10 @@ var init_template_walker = __esm({
5359
5531
  "rules/README.md",
5360
5532
  "settings.project.base.json",
5361
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",
5362
5538
  // CBP-internal hooks — see templates/hooks/README.md "Hooks NOT included and why"
5363
5539
  "hooks/validate-structure.sh",
5364
5540
  "hooks/validate-structure-lib.sh",
@@ -5376,15 +5552,15 @@ var init_template_walker = __esm({
5376
5552
  // src/lib/manifest.ts
5377
5553
  import * as fs2 from "node:fs";
5378
5554
  import * as os from "node:os";
5379
- import * as path2 from "node:path";
5555
+ import * as path3 from "node:path";
5380
5556
  function manifestPath(projectDir) {
5381
- return path2.join(projectDir, ".claude", NEW_MANIFEST_FILENAME);
5557
+ return path3.join(projectDir, ".claude", NEW_MANIFEST_FILENAME);
5382
5558
  }
5383
5559
  function midManifestPath(projectDir) {
5384
- return path2.join(projectDir, ".claude", MID_MANIFEST_FILENAME);
5560
+ return path3.join(projectDir, ".claude", MID_MANIFEST_FILENAME);
5385
5561
  }
5386
5562
  function oldManifestPath(projectDir) {
5387
- return path2.join(projectDir, ".claude", OLD_MANIFEST_FILENAME);
5563
+ return path3.join(projectDir, ".claude", OLD_MANIFEST_FILENAME);
5388
5564
  }
5389
5565
  function readManifest(projectDir) {
5390
5566
  const newFile = manifestPath(projectDir);
@@ -5406,7 +5582,7 @@ function readManifest(projectDir) {
5406
5582
  }
5407
5583
  function writeManifest(projectDir, manifest) {
5408
5584
  const file = manifestPath(projectDir);
5409
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
5585
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
5410
5586
  fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
5411
5587
  const mid = midManifestPath(projectDir);
5412
5588
  if (fs2.existsSync(mid)) {
@@ -5425,16 +5601,16 @@ function defaultManifest() {
5425
5601
  };
5426
5602
  }
5427
5603
  function userManifestPath(userDir) {
5428
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5429
- return path2.join(dir, NEW_MANIFEST_FILENAME);
5604
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5605
+ return path3.join(dir, NEW_MANIFEST_FILENAME);
5430
5606
  }
5431
5607
  function userMidManifestPath(userDir) {
5432
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5433
- return path2.join(dir, MID_MANIFEST_FILENAME);
5608
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5609
+ return path3.join(dir, MID_MANIFEST_FILENAME);
5434
5610
  }
5435
5611
  function userOldManifestPath(userDir) {
5436
- const dir = userDir ?? path2.join(os.homedir(), ".claude");
5437
- return path2.join(dir, OLD_MANIFEST_FILENAME);
5612
+ const dir = userDir ?? path3.join(os.homedir(), ".claude");
5613
+ return path3.join(dir, OLD_MANIFEST_FILENAME);
5438
5614
  }
5439
5615
  function readManifestForScope(scope, arg2) {
5440
5616
  if (scope === "user") {
@@ -5460,7 +5636,7 @@ function readManifestForScope(scope, arg2) {
5460
5636
  function writeManifestForScope(scope, manifest, arg3) {
5461
5637
  if (scope === "user") {
5462
5638
  const file = userManifestPath(arg3);
5463
- fs2.mkdirSync(path2.dirname(file), { recursive: true });
5639
+ fs2.mkdirSync(path3.dirname(file), { recursive: true });
5464
5640
  fs2.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n", "utf8");
5465
5641
  const mid = userMidManifestPath(arg3);
5466
5642
  if (fs2.existsSync(mid)) {
@@ -5716,14 +5892,14 @@ __export(install_exports, {
5716
5892
  });
5717
5893
  import * as fs3 from "node:fs";
5718
5894
  import * as os2 from "node:os";
5719
- import * as path3 from "node:path";
5895
+ import * as path4 from "node:path";
5720
5896
  import { fileURLToPath } from "node:url";
5721
5897
  function resolveTemplatesDir() {
5722
- const here = path3.dirname(fileURLToPath(import.meta.url));
5898
+ const here = path4.dirname(fileURLToPath(import.meta.url));
5723
5899
  const candidates = [
5724
- path3.resolve(here, "..", "templates"),
5725
- path3.resolve(here, "..", "..", "templates"),
5726
- path3.resolve(here, "..", "..", "..", "templates")
5900
+ path4.resolve(here, "..", "templates"),
5901
+ path4.resolve(here, "..", "..", "templates"),
5902
+ path4.resolve(here, "..", "..", "..", "templates")
5727
5903
  ];
5728
5904
  for (const c of candidates) {
5729
5905
  if (fs3.existsSync(c) && fs3.statSync(c).isDirectory()) {
@@ -5764,14 +5940,14 @@ async function runInstall(opts, deps = {}) {
5764
5940
  const files = walkTemplates(templatesDir);
5765
5941
  const manifestEntries = [];
5766
5942
  for (const f of files) {
5767
- const absDest = path3.join(projectDir, ".claude", f.dest);
5768
- const absSrc = path3.join(templatesDir, f.src);
5943
+ const absDest = path4.join(projectDir, ".claude", f.dest);
5944
+ const absSrc = path4.join(templatesDir, f.src);
5769
5945
  if (opts.dryRun) {
5770
5946
  if (opts.verbose) {
5771
5947
  console.log(`[dry-run] would copy ${f.src} \u2192 .claude/${f.dest}`);
5772
5948
  }
5773
5949
  } else {
5774
- fs3.mkdirSync(path3.dirname(absDest), { recursive: true });
5950
+ fs3.mkdirSync(path4.dirname(absDest), { recursive: true });
5775
5951
  fs3.copyFileSync(absSrc, absDest);
5776
5952
  if (opts.verbose) {
5777
5953
  console.log(`copied ${f.src} \u2192 .claude/${f.dest}`);
@@ -5779,15 +5955,15 @@ async function runInstall(opts, deps = {}) {
5779
5955
  }
5780
5956
  manifestEntries.push({ src: f.src, dest: f.dest, hash: f.hash });
5781
5957
  }
5782
- const hooksJsonPath = path3.join(templatesDir, "hooks", "hooks.json");
5783
- const baseSettingsPath = path3.join(
5958
+ const hooksJsonPath = path4.join(templatesDir, "hooks", "hooks.json");
5959
+ const baseSettingsPath = path4.join(
5784
5960
  templatesDir,
5785
5961
  "settings.project.base.json"
5786
5962
  );
5787
5963
  const hasHooks = fs3.existsSync(hooksJsonPath);
5788
5964
  const hasBase = fs3.existsSync(baseSettingsPath);
5789
5965
  if (hasHooks || hasBase) {
5790
- const settingsPath = path3.join(projectDir, ".claude", "settings.json");
5966
+ const settingsPath = path4.join(projectDir, ".claude", "settings.json");
5791
5967
  const existingSettings = fs3.existsSync(settingsPath) ? JSON.parse(fs3.readFileSync(settingsPath, "utf8")) : {};
5792
5968
  if (hasBase) {
5793
5969
  const base = JSON.parse(
@@ -5802,7 +5978,7 @@ async function runInstall(opts, deps = {}) {
5802
5978
  mergeHooksIntoSettings(existingSettings, hooksJson);
5803
5979
  }
5804
5980
  if (!opts.dryRun) {
5805
- fs3.mkdirSync(path3.dirname(settingsPath), { recursive: true });
5981
+ fs3.mkdirSync(path4.dirname(settingsPath), { recursive: true });
5806
5982
  fs3.writeFileSync(
5807
5983
  settingsPath,
5808
5984
  JSON.stringify(existingSettings, null, 2) + "\n",
@@ -5810,10 +5986,19 @@ async function runInstall(opts, deps = {}) {
5810
5986
  );
5811
5987
  } else if (opts.verbose) {
5812
5988
  console.log(
5813
- `[dry-run] would merge settings into ${path3.relative(projectDir, settingsPath)}`
5989
+ `[dry-run] would merge settings into ${path4.relative(projectDir, settingsPath)}`
5814
5990
  );
5815
5991
  }
5816
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
+ }
5817
6002
  if (!opts.dryRun) {
5818
6003
  const manifest = defaultManifest();
5819
6004
  manifest.files = manifestEntries;
@@ -5844,9 +6029,9 @@ function runInstallUser(opts, deps) {
5844
6029
  return;
5845
6030
  }
5846
6031
  try {
5847
- const userDir = deps.userDir ?? path3.join(os2.homedir(), ".claude");
5848
- const settingsPath = path3.join(userDir, "settings.json");
5849
- 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(
5850
6035
  templatesDir,
5851
6036
  "settings.user.base.json"
5852
6037
  );
@@ -5888,7 +6073,7 @@ function runInstallUser(opts, deps) {
5888
6073
  }
5889
6074
  }
5890
6075
  function countHookEntries(templatesDir) {
5891
- const p = path3.join(templatesDir, "hooks", "hooks.json");
6076
+ const p = path4.join(templatesDir, "hooks", "hooks.json");
5892
6077
  if (!fs3.existsSync(p)) return 0;
5893
6078
  try {
5894
6079
  const j = JSON.parse(fs3.readFileSync(p, "utf8"));
@@ -5908,6 +6093,7 @@ var init_install = __esm({
5908
6093
  "src/cli/claude/install.ts"() {
5909
6094
  "use strict";
5910
6095
  init_template_walker();
6096
+ init_gitignore_block();
5911
6097
  init_manifest();
5912
6098
  init_settings_merge();
5913
6099
  init_statusline_config();
@@ -6032,7 +6218,7 @@ __export(update_exports, {
6032
6218
  });
6033
6219
  import * as fs4 from "node:fs";
6034
6220
  import * as os3 from "node:os";
6035
- import * as path4 from "node:path";
6221
+ import * as path5 from "node:path";
6036
6222
  import { fileURLToPath as fileURLToPath2 } from "node:url";
6037
6223
  async function runUpdate(opts, deps = {}) {
6038
6224
  await Promise.resolve();
@@ -6072,9 +6258,9 @@ async function runUpdate(opts, deps = {}) {
6072
6258
  finalManifestEntries.push(e);
6073
6259
  }
6074
6260
  for (const { packaged, absSrc } of plan.overwriteSafe) {
6075
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6261
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6076
6262
  if (!opts.dryRun) {
6077
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6263
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6078
6264
  fs4.copyFileSync(absSrc, absDest);
6079
6265
  if (opts.verbose) console.log(`updated ${packaged.dest}`);
6080
6266
  } else if (opts.verbose) {
@@ -6087,7 +6273,7 @@ async function runUpdate(opts, deps = {}) {
6087
6273
  absSrc,
6088
6274
  onDiskContent
6089
6275
  } of plan.overwriteHandEdited) {
6090
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6276
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6091
6277
  const newContent = fs4.readFileSync(absSrc);
6092
6278
  const showDiff = () => {
6093
6279
  console.log(
@@ -6100,7 +6286,7 @@ async function runUpdate(opts, deps = {}) {
6100
6286
  const answer = await promptOverwrite(packaged.dest, opts, showDiff);
6101
6287
  if (answer === "overwrite") {
6102
6288
  if (!opts.dryRun) {
6103
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6289
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6104
6290
  fs4.copyFileSync(absSrc, absDest);
6105
6291
  }
6106
6292
  finalManifestEntries.push(packaged);
@@ -6116,9 +6302,9 @@ async function runUpdate(opts, deps = {}) {
6116
6302
  for (const { packaged, absSrc } of plan.newOptIn) {
6117
6303
  const answer = await promptOptIn(packaged.dest, opts);
6118
6304
  if (answer === "opt-in") {
6119
- const absDest = path4.join(projectDir, ".claude", packaged.dest);
6305
+ const absDest = path5.join(projectDir, ".claude", packaged.dest);
6120
6306
  if (!opts.dryRun) {
6121
- fs4.mkdirSync(path4.dirname(absDest), { recursive: true });
6307
+ fs4.mkdirSync(path5.dirname(absDest), { recursive: true });
6122
6308
  fs4.copyFileSync(absSrc, absDest);
6123
6309
  }
6124
6310
  finalManifestEntries.push(packaged);
@@ -6130,25 +6316,25 @@ async function runUpdate(opts, deps = {}) {
6130
6316
  for (const e of plan.removedFromPackage) {
6131
6317
  const answer = await promptRemove(e.dest, opts);
6132
6318
  if (answer === "remove") {
6133
- const absDest = path4.join(projectDir, ".claude", e.dest);
6319
+ const absDest = path5.join(projectDir, ".claude", e.dest);
6134
6320
  if (!opts.dryRun && fs4.existsSync(absDest)) {
6135
6321
  fs4.rmSync(absDest);
6136
- const claudeDir = path4.join(projectDir, ".claude");
6137
- let cur = path4.dirname(absDest);
6138
- while (cur !== claudeDir && cur !== path4.dirname(cur)) {
6139
- 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;
6140
6326
  try {
6141
6327
  fs4.rmdirSync(cur);
6142
6328
  if (opts.verbose)
6143
6329
  console.log(
6144
- `pruned empty dir ${path4.relative(claudeDir, cur)}`
6330
+ `pruned empty dir ${path5.relative(claudeDir, cur)}`
6145
6331
  );
6146
- cur = path4.dirname(cur);
6332
+ cur = path5.dirname(cur);
6147
6333
  } catch (err) {
6148
6334
  const code = err.code;
6149
6335
  if (code !== "ENOTEMPTY" && code !== "ENOENT") {
6150
6336
  console.warn(
6151
- `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}`
6152
6338
  );
6153
6339
  }
6154
6340
  break;
@@ -6160,16 +6346,16 @@ async function runUpdate(opts, deps = {}) {
6160
6346
  if (opts.verbose) console.log(`kept (untracked) ${e.dest}`);
6161
6347
  }
6162
6348
  }
6163
- const hooksJsonPath = path4.join(templatesDir, "hooks", "hooks.json");
6349
+ const hooksJsonPath = path5.join(templatesDir, "hooks", "hooks.json");
6164
6350
  if (fs4.existsSync(hooksJsonPath)) {
6165
6351
  const hooksJson = JSON.parse(
6166
6352
  fs4.readFileSync(hooksJsonPath, "utf8")
6167
6353
  );
6168
- const settingsPath = path4.join(projectDir, ".claude", "settings.json");
6354
+ const settingsPath = path5.join(projectDir, ".claude", "settings.json");
6169
6355
  const existingSettings = fs4.existsSync(settingsPath) ? JSON.parse(fs4.readFileSync(settingsPath, "utf8")) : {};
6170
6356
  mergeHooksIntoSettings(existingSettings, hooksJson);
6171
6357
  if (!opts.dryRun) {
6172
- fs4.mkdirSync(path4.dirname(settingsPath), { recursive: true });
6358
+ fs4.mkdirSync(path5.dirname(settingsPath), { recursive: true });
6173
6359
  fs4.writeFileSync(
6174
6360
  settingsPath,
6175
6361
  JSON.stringify(existingSettings, null, 2) + "\n",
@@ -6177,6 +6363,15 @@ async function runUpdate(opts, deps = {}) {
6177
6363
  );
6178
6364
  }
6179
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
+ }
6180
6375
  if (!opts.dryRun) {
6181
6376
  const manifest = defaultManifest();
6182
6377
  manifest.files = finalManifestEntries.sort(
@@ -6209,9 +6404,9 @@ function runUpdateUser(opts, deps) {
6209
6404
  return;
6210
6405
  }
6211
6406
  try {
6212
- const userDir = deps.userDir ?? path4.join(os3.homedir(), ".claude");
6213
- const settingsPath = path4.join(userDir, "settings.json");
6214
- 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(
6215
6410
  templatesDir,
6216
6411
  "settings.user.base.json"
6217
6412
  );
@@ -6273,8 +6468,8 @@ function buildPlan(projectDir, templatesDir, manifest) {
6273
6468
  };
6274
6469
  for (const pkg of packaged) {
6275
6470
  const inManifest = manifestBySrc.get(pkg.src);
6276
- const absDest = path4.join(projectDir, ".claude", pkg.dest);
6277
- const absSrc = path4.join(templatesDir, pkg.src);
6471
+ const absDest = path5.join(projectDir, ".claude", pkg.dest);
6472
+ const absSrc = path5.join(templatesDir, pkg.src);
6278
6473
  if (!inManifest) {
6279
6474
  plan.newOptIn.push({
6280
6475
  packaged: { src: pkg.src, dest: pkg.dest, hash: pkg.hash },
@@ -6310,11 +6505,11 @@ function buildPlan(projectDir, templatesDir, manifest) {
6310
6505
  return plan;
6311
6506
  }
6312
6507
  function resolveTemplatesDirFromInstall() {
6313
- const here = path4.dirname(fileURLToPath2(import.meta.url));
6508
+ const here = path5.dirname(fileURLToPath2(import.meta.url));
6314
6509
  const candidates = [
6315
- path4.resolve(here, "..", "templates"),
6316
- path4.resolve(here, "..", "..", "templates"),
6317
- path4.resolve(here, "..", "..", "..", "templates")
6510
+ path5.resolve(here, "..", "templates"),
6511
+ path5.resolve(here, "..", "..", "templates"),
6512
+ path5.resolve(here, "..", "..", "..", "templates")
6318
6513
  ];
6319
6514
  for (const c of candidates) {
6320
6515
  if (fs4.existsSync(c) && fs4.statSync(c).isDirectory()) {
@@ -6332,6 +6527,7 @@ var init_update = __esm({
6332
6527
  "src/cli/claude/update.ts"() {
6333
6528
  "use strict";
6334
6529
  init_template_walker();
6530
+ init_gitignore_block();
6335
6531
  init_hash();
6336
6532
  init_manifest();
6337
6533
  init_settings_merge();
@@ -6347,7 +6543,7 @@ __export(uninstall_exports, {
6347
6543
  });
6348
6544
  import * as fs5 from "node:fs";
6349
6545
  import * as os4 from "node:os";
6350
- import * as path5 from "node:path";
6546
+ import * as path6 from "node:path";
6351
6547
  async function runUninstall(opts, deps = {}) {
6352
6548
  await Promise.resolve();
6353
6549
  const scope = opts.scope ?? "project";
@@ -6376,7 +6572,7 @@ async function runUninstall(opts, deps = {}) {
6376
6572
  let removed = 0;
6377
6573
  let warnings = 0;
6378
6574
  for (const entry of manifest.files) {
6379
- const abs = path5.join(projectDir, ".claude", entry.dest);
6575
+ const abs = path6.join(projectDir, ".claude", entry.dest);
6380
6576
  if (!fs5.existsSync(abs)) {
6381
6577
  console.warn(
6382
6578
  `codebyplan claude uninstall: ${entry.dest} already absent (skipping).`
@@ -6400,12 +6596,12 @@ async function runUninstall(opts, deps = {}) {
6400
6596
  if (!opts.dryRun) {
6401
6597
  pruneEmptyManagedDirs(projectDir);
6402
6598
  }
6403
- const settingsPath = path5.join(projectDir, ".claude", "settings.json");
6599
+ const settingsPath = path6.join(projectDir, ".claude", "settings.json");
6404
6600
  if (fs5.existsSync(settingsPath)) {
6405
6601
  const settings = JSON.parse(
6406
6602
  fs5.readFileSync(settingsPath, "utf8")
6407
6603
  );
6408
- const baseSettingsPath = templatesDir ? path5.join(templatesDir, "settings.project.base.json") : null;
6604
+ const baseSettingsPath = templatesDir ? path6.join(templatesDir, "settings.project.base.json") : null;
6409
6605
  if (baseSettingsPath && fs5.existsSync(baseSettingsPath)) {
6410
6606
  const base = JSON.parse(
6411
6607
  fs5.readFileSync(baseSettingsPath, "utf8")
@@ -6426,6 +6622,15 @@ async function runUninstall(opts, deps = {}) {
6426
6622
  }
6427
6623
  }
6428
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
+ }
6429
6634
  if (!opts.dryRun) {
6430
6635
  const m = manifestPath(projectDir);
6431
6636
  if (fs5.existsSync(m)) fs5.rmSync(m);
@@ -6454,7 +6659,7 @@ function runUninstallUser(opts, deps) {
6454
6659
  }
6455
6660
  }
6456
6661
  try {
6457
- const userDir = deps.userDir ?? path5.join(os4.homedir(), ".claude");
6662
+ const userDir = deps.userDir ?? path6.join(os4.homedir(), ".claude");
6458
6663
  const existingManifest = readManifestForScope("user", userDir);
6459
6664
  if (!existingManifest) {
6460
6665
  console.error(
@@ -6463,12 +6668,12 @@ function runUninstallUser(opts, deps) {
6463
6668
  process.exitCode = 1;
6464
6669
  return;
6465
6670
  }
6466
- const settingsPath = path5.join(userDir, "settings.json");
6671
+ const settingsPath = path6.join(userDir, "settings.json");
6467
6672
  if (fs5.existsSync(settingsPath)) {
6468
6673
  const settings = JSON.parse(
6469
6674
  fs5.readFileSync(settingsPath, "utf8")
6470
6675
  );
6471
- 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;
6472
6677
  if (userBaseSettingsPath && fs5.existsSync(userBaseSettingsPath)) {
6473
6678
  const userBase = JSON.parse(
6474
6679
  fs5.readFileSync(userBaseSettingsPath, "utf8")
@@ -6509,7 +6714,7 @@ function runUninstallUser(opts, deps) {
6509
6714
  function pruneEmptyManagedDirs(projectDir) {
6510
6715
  const managedRoots = ["skills", "agents", "hooks", "rules"];
6511
6716
  for (const root of managedRoots) {
6512
- const abs = path5.join(projectDir, ".claude", root);
6717
+ const abs = path6.join(projectDir, ".claude", root);
6513
6718
  if (!fs5.existsSync(abs)) continue;
6514
6719
  pruneLeafFirst(abs);
6515
6720
  }
@@ -6520,7 +6725,7 @@ function pruneLeafFirst(dir) {
6520
6725
  if (!stat2.isDirectory()) return;
6521
6726
  for (const entry of fs5.readdirSync(dir, { withFileTypes: true })) {
6522
6727
  if (entry.isDirectory()) {
6523
- pruneLeafFirst(path5.join(dir, entry.name));
6728
+ pruneLeafFirst(path6.join(dir, entry.name));
6524
6729
  }
6525
6730
  }
6526
6731
  const remaining = fs5.readdirSync(dir);
@@ -6532,6 +6737,7 @@ var init_uninstall = __esm({
6532
6737
  "src/cli/claude/uninstall.ts"() {
6533
6738
  "use strict";
6534
6739
  init_hash();
6740
+ init_gitignore_block();
6535
6741
  init_manifest();
6536
6742
  init_settings_merge();
6537
6743
  init_install();