@tablation/crew 0.85.0 → 0.87.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 (2) hide show
  1. package/dist/cli.js +888 -494
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -56,25 +56,107 @@ async function previewHostProblem(cfg, resolve6 = (h) => lookup(h)) {
56
56
  }
57
57
  return void 0;
58
58
  }
59
+ function routePeriscopeProblems(o) {
60
+ if (!o.hosts) return [];
61
+ const out = [];
62
+ for (const r of o.repos) {
63
+ if (!r.hasPreviewHook) {
64
+ out.push({
65
+ severity: "note",
66
+ kind: "preview_hook_missing",
67
+ route: o.route,
68
+ repo: r.name,
69
+ message: `${o.route}/${r.name}: hostPeriscopes is on but ${r.name} declares no \`preview\` hook, so it cannot be opened as a periscope`
70
+ });
71
+ }
72
+ }
73
+ if (o.tablePresent === false) {
74
+ out.push({
75
+ severity: "blocker",
76
+ kind: "periscopes_table_missing",
77
+ route: o.route,
78
+ message: `${o.route}: hostPeriscopes is on but this workspace has no Periscopes table \u2014 update the Issues template and run \`crew connect\``
79
+ });
80
+ }
81
+ return out;
82
+ }
83
+ function previewHostFinding(message) {
84
+ return /does not resolve/.test(message) ? { severity: "attention", kind: "preview_host_unresolvable", message } : { severity: "blocker", kind: "preview_host", message };
85
+ }
86
+
87
+ // src/periscope-state.ts
88
+ import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "fs";
89
+ import { connect } from "net";
90
+ import { join } from "path";
91
+ var dirOf = (stateDir) => join(stateDir, "periscopes");
92
+ var fileOf = (stateDir, ticketRecordId) => join(dirOf(stateDir), `${ticketRecordId}.json`);
93
+ function writePeriscopeSidecar(stateDir, sc) {
94
+ mkdirSync(dirOf(stateDir), { recursive: true });
95
+ writeFileSync(fileOf(stateDir, sc.ticketRecordId), `${JSON.stringify(sc, null, 2)}
96
+ `);
97
+ }
98
+ function removePeriscopeSidecar(stateDir, ticketRecordId) {
99
+ rmSync(fileOf(stateDir, ticketRecordId), { force: true });
100
+ }
101
+ function readPeriscopeSidecars(stateDir) {
102
+ const dir = dirOf(stateDir);
103
+ if (!existsSync2(dir)) return [];
104
+ const out = [];
105
+ for (const name of readdirSync(dir)) {
106
+ if (!name.endsWith(".json")) continue;
107
+ try {
108
+ const sc = JSON.parse(readFileSync(join(dir, name), "utf8"));
109
+ if (sc && typeof sc.ticketRecordId === "string" && typeof sc.url === "string") out.push(sc);
110
+ } catch {
111
+ }
112
+ }
113
+ return out.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
114
+ }
115
+ function urlPort(url) {
116
+ try {
117
+ const u = new URL(url);
118
+ return u.port ? Number(u.port) : u.protocol === "https:" ? 443 : 80;
119
+ } catch {
120
+ return void 0;
121
+ }
122
+ }
123
+ function portListening(port, timeoutMs = 500) {
124
+ return new Promise((resolve6) => {
125
+ const s = connect({ port, host: "127.0.0.1" });
126
+ const done = (ok) => {
127
+ s.destroy();
128
+ resolve6(ok);
129
+ };
130
+ s.setTimeout(timeoutMs, () => done(false));
131
+ s.once("connect", () => done(true));
132
+ s.once("error", () => done(false));
133
+ });
134
+ }
135
+ async function periscopesForStatus(stateDir, probe = portListening) {
136
+ return Promise.all(readPeriscopeSidecars(stateDir).map(async (sc) => {
137
+ const port = urlPort(sc.url);
138
+ return { ...sc, serving: port === void 0 ? false : await probe(port) };
139
+ }));
140
+ }
59
141
 
60
142
  // src/config.ts
61
- import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
62
- import { resolve, dirname as dirname3, isAbsolute, join as join9, basename as basename3 } from "path";
143
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
144
+ import { resolve, dirname as dirname3, isAbsolute, join as join10, basename as basename3 } from "path";
63
145
  import { homedir as homedir6, tmpdir } from "os";
64
146
  import { parse as parse2 } from "yaml";
65
147
 
66
148
  // src/events.ts
67
149
  import {
68
150
  appendFileSync,
69
- mkdirSync,
70
- existsSync as existsSync2,
151
+ mkdirSync as mkdirSync2,
152
+ existsSync as existsSync3,
71
153
  statSync,
72
154
  renameSync,
73
155
  openSync,
74
156
  closeSync,
75
- rmSync
157
+ rmSync as rmSync2
76
158
  } from "fs";
77
- import { join, dirname } from "path";
159
+ import { join as join2, dirname } from "path";
78
160
  function render(e) {
79
161
  const scope = e.role ? `${e.step}[${e.role}]` : e.step;
80
162
  const ticket = e.ticket ? ` ${e.ticket}` : "";
@@ -86,7 +168,7 @@ var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
86
168
  var ROTATE_LOCK_STALE_MS = 6e4;
87
169
  function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
88
170
  try {
89
- if (!existsSync2(file) || statSync(file).size <= maxBytes) return void 0;
171
+ if (!existsSync3(file) || statSync(file).size <= maxBytes) return void 0;
90
172
  } catch {
91
173
  return void 0;
92
174
  }
@@ -97,7 +179,7 @@ function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
97
179
  } catch {
98
180
  try {
99
181
  if (Date.now() - statSync(lock).mtimeMs < ROTATE_LOCK_STALE_MS) return void 0;
100
- rmSync(lock, { force: true });
182
+ rmSync2(lock, { force: true });
101
183
  closeSync(openSync(lock, "wx"));
102
184
  } catch {
103
185
  return void 0;
@@ -106,12 +188,12 @@ function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
106
188
  try {
107
189
  if (statSync(file).size <= maxBytes) return void 0;
108
190
  const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
109
- let dest = join(dirname(file), `events.${stamp}.jsonl`);
110
- for (let n = 1; existsSync2(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
191
+ let dest = join2(dirname(file), `events.${stamp}.jsonl`);
192
+ for (let n = 1; existsSync3(dest); n++) dest = join2(dirname(file), `events.${stamp}-${n}.jsonl`);
111
193
  renameSync(file, dest);
112
194
  return dest;
113
195
  } finally {
114
- rmSync(lock, { force: true });
196
+ rmSync2(lock, { force: true });
115
197
  }
116
198
  } catch {
117
199
  return void 0;
@@ -130,7 +212,7 @@ var Emitter = class _Emitter {
130
212
  for (const f of [opts.eventFile, opts.logFile]) {
131
213
  if (!f) continue;
132
214
  try {
133
- mkdirSync(dirname(f), { recursive: true });
215
+ mkdirSync2(dirname(f), { recursive: true });
134
216
  } catch {
135
217
  }
136
218
  }
@@ -206,18 +288,18 @@ var Emitter = class _Emitter {
206
288
  }
207
289
  }
208
290
  };
209
- var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
291
+ var eventFileFor = (stateDir) => join2(stateDir, "events.jsonl");
210
292
 
211
293
  // src/pair-install.ts
212
- import { existsSync as existsSync5, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
294
+ import { existsSync as existsSync6, rmSync as rmSync5, writeFileSync as writeFileSync5 } from "fs";
213
295
  import { spawnSync as spawnSync3 } from "child_process";
214
296
  import { homedir as homedir5 } from "os";
215
- import { join as join7 } from "path";
297
+ import { join as join8 } from "path";
216
298
 
217
299
  // src/adapters/claude.ts
218
- import { readFileSync as readFileSync5 } from "fs";
300
+ import { readFileSync as readFileSync6 } from "fs";
219
301
  import { homedir as homedir3 } from "os";
220
- import { join as join5 } from "path";
302
+ import { join as join6 } from "path";
221
303
 
222
304
  // src/stream.ts
223
305
  function toolTarget(name, input) {
@@ -276,19 +358,19 @@ function extractResult(line) {
276
358
 
277
359
  // src/claude-mcp.ts
278
360
  import { spawnSync } from "child_process";
279
- import { readFileSync as readFileSync3 } from "fs";
361
+ import { readFileSync as readFileSync4 } from "fs";
280
362
  import { homedir } from "os";
281
- import { join as join3 } from "path";
363
+ import { join as join4 } from "path";
282
364
 
283
365
  // src/mcp-registrations.ts
284
366
  import { createHash } from "crypto";
285
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
286
- import { join as join2 } from "path";
287
- var fileFor = (stateDir) => join2(stateDir, "mcp-registrations.json");
367
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
368
+ import { join as join3 } from "path";
369
+ var fileFor = (stateDir) => join3(stateDir, "mcp-registrations.json");
288
370
  var keyFingerprint = (key) => createHash("sha256").update(key).digest("hex").slice(0, 8);
289
371
  function readMcpRegistrations(stateDir) {
290
372
  try {
291
- const j = JSON.parse(readFileSync2(fileFor(stateDir), "utf8"));
373
+ const j = JSON.parse(readFileSync3(fileFor(stateDir), "utf8"));
292
374
  return Array.isArray(j) ? j : [];
293
375
  } catch {
294
376
  return [];
@@ -296,8 +378,8 @@ function readMcpRegistrations(stateDir) {
296
378
  }
297
379
  function recordMcpRegistration(stateDir, r, now = /* @__PURE__ */ new Date()) {
298
380
  const rest = readMcpRegistrations(stateDir).filter((x) => !(x.harness === r.harness && x.scope === r.scope && x.name === r.name));
299
- mkdirSync2(stateDir, { recursive: true });
300
- writeFileSync(fileFor(stateDir), `${JSON.stringify([...rest, { ...r, at: now.toISOString() }], null, 2)}
381
+ mkdirSync3(stateDir, { recursive: true });
382
+ writeFileSync2(fileFor(stateDir), `${JSON.stringify([...rest, { ...r, at: now.toISOString() }], null, 2)}
301
383
  `, { mode: 384 });
302
384
  }
303
385
  function findMcpRegistration(stateDir, r) {
@@ -320,7 +402,7 @@ function runClaude(bin, args) {
320
402
  }
321
403
  function userScopeEntry(home) {
322
404
  try {
323
- const j = JSON.parse(readFileSync3(join3(home, ".claude.json"), "utf8"));
405
+ const j = JSON.parse(readFileSync4(join4(home, ".claude.json"), "utf8"));
324
406
  return j.mcpServers?.[MCP_SERVER_NAME];
325
407
  } catch {
326
408
  return void 0;
@@ -404,8 +486,8 @@ function removeClaudeMcp(o) {
404
486
  }
405
487
 
406
488
  // src/launcher.ts
407
- import { basename, join as join4 } from "path";
408
- import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync } from "fs";
489
+ import { basename, join as join5 } from "path";
490
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
409
491
  import { homedir as homedir2 } from "os";
410
492
  function isOriLauncher(spec) {
411
493
  return !!spec.launcher && basename(spec.launcher) === "ori";
@@ -414,7 +496,7 @@ function isOmlxLauncher(spec) {
414
496
  return !!spec.launcher && basename(spec.launcher) === "omlx";
415
497
  }
416
498
  function defaultOmlxBin(home = homedir2()) {
417
- return join4(home, ".omlx", "bin", "omlx");
499
+ return join5(home, ".omlx", "bin", "omlx");
418
500
  }
419
501
  function launcherExecutable(spec, home) {
420
502
  if (!spec.launcher) return void 0;
@@ -460,15 +542,15 @@ function interpretOmlxProbe(status2) {
460
542
  function omlxModelPresent(model, dirs) {
461
543
  return dirs.some((d) => {
462
544
  try {
463
- return readdirSync(d).includes(model);
545
+ return readdirSync2(d).includes(model);
464
546
  } catch {
465
547
  return false;
466
548
  }
467
549
  });
468
550
  }
469
551
  function readOmlxSettings(home = homedir2()) {
470
- const p = join4(home, ".omlx", "settings.json");
471
- return existsSync3(p) ? parseOmlxSettings(readFileSync4(p, "utf8")) : void 0;
552
+ const p = join5(home, ".omlx", "settings.json");
553
+ return existsSync4(p) ? parseOmlxSettings(readFileSync5(p, "utf8")) : void 0;
472
554
  }
473
555
 
474
556
  // src/capability.ts
@@ -526,7 +608,7 @@ function describeClaudeAuth(spec, env = process.env, home = homedir3()) {
526
608
  if (spec.auth === "subscription") return "subscription (settings override)";
527
609
  let fromSettings;
528
610
  try {
529
- const parsed = JSON.parse(readFileSync5(join5(home, ".claude", "settings.json"), "utf8"));
611
+ const parsed = JSON.parse(readFileSync6(join6(home, ".claude", "settings.json"), "utf8"));
530
612
  const e = parsed.env ?? {};
531
613
  const on = (k) => e[k] !== void 0 && e[k] !== "" && e[k] !== "0";
532
614
  if (on("CLAUDE_CODE_USE_VERTEX")) fromSettings = "vertex from ~/.claude/settings.json";
@@ -622,11 +704,11 @@ var claudeAdapter = {
622
704
  // src/adapters/codex.ts
623
705
  import { spawnSync as spawnSync2 } from "child_process";
624
706
  import { homedir as homedir4 } from "os";
625
- import { join as join6 } from "path";
626
- import { rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
707
+ import { join as join7 } from "path";
708
+ import { rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
627
709
 
628
710
  // src/pair-files.ts
629
- import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync2, rmSync as rmSync2 } from "fs";
711
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync3, rmSync as rmSync3 } from "fs";
630
712
  import { dirname as dirname2 } from "path";
631
713
  var PAIR_BLOCK_START = "<!-- crew:pair start -->";
632
714
  var PAIR_BLOCK_END = "<!-- crew:pair end -->";
@@ -667,11 +749,11 @@ function removePairBlock(text) {
667
749
  return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
668
750
  }
669
751
  function readIfExists(path) {
670
- return existsSync4(path) ? readFileSync6(path, "utf8") : void 0;
752
+ return existsSync5(path) ? readFileSync7(path, "utf8") : void 0;
671
753
  }
672
754
  function writeFileMkdir(path, text) {
673
- mkdirSync3(dirname2(path), { recursive: true });
674
- writeFileSync2(path, text);
755
+ mkdirSync4(dirname2(path), { recursive: true });
756
+ writeFileSync3(path, text);
675
757
  }
676
758
  function installBlockFile(path, harness, dryRun) {
677
759
  const before = readIfExists(path) ?? "";
@@ -686,8 +768,8 @@ function removeBlockFile(path, dryRun) {
686
768
  if (before === void 0 || !hasPairBlock(before)) return false;
687
769
  if (dryRun) return true;
688
770
  const after = removePairBlock(before);
689
- if (after.trim() === "") rmSync2(path);
690
- else writeFileSync2(path, after);
771
+ if (after.trim() === "") rmSync3(path);
772
+ else writeFileSync3(path, after);
691
773
  return true;
692
774
  }
693
775
  function blockVerdict(path, harness) {
@@ -814,8 +896,8 @@ function codexTable(url) {
814
896
  return [`[mcp_servers.${SERVER}]`, `url = ${tomlString(url)}`, `bearer_token_env_var = ${tomlString(PAIR_KEY_VAR)}`];
815
897
  }
816
898
  var codexPaths = (home) => ({
817
- instructions: join6(home, ".codex", "AGENTS.md"),
818
- config: join6(home, ".codex", "config.toml")
899
+ instructions: join7(home, ".codex", "AGENTS.md"),
900
+ config: join7(home, ".codex", "config.toml")
819
901
  });
820
902
  function codexMcpState(config, url) {
821
903
  const lines = config.split("\n");
@@ -865,8 +947,8 @@ var codexPair = {
865
947
  const kept = st.state === "different" ? `; left the existing ${SERVER} server alone \u2014 ${st.detail}` : "";
866
948
  if (hadMcp && !o.dryRun) {
867
949
  const after = removeTomlTable(config, `mcp_servers.${SERVER}`);
868
- if (after === "") rmSync3(p.config);
869
- else writeFileSync3(p.config, after);
950
+ if (after === "") rmSync4(p.config);
951
+ else writeFileSync4(p.config, after);
870
952
  }
871
953
  const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
872
954
  return hadBlock || hadMcp ? `pair(codex): ${o.dryRun ? "(dry run) would remove" : "removed"} ${what}${kept}` : `pair(codex): nothing to remove${kept}`;
@@ -962,8 +1044,8 @@ var codexAdapter = {
962
1044
 
963
1045
  // src/pair-install.ts
964
1046
  var geminiPaths = (home) => ({
965
- instructions: join7(home, ".gemini", "GEMINI.md"),
966
- config: join7(home, ".gemini", "settings.json")
1047
+ instructions: join8(home, ".gemini", "GEMINI.md"),
1048
+ config: join8(home, ".gemini", "settings.json")
967
1049
  });
968
1050
  function readGeminiSettings(path) {
969
1051
  const text = readIfExists(path);
@@ -1050,8 +1132,8 @@ var geminiPair = {
1050
1132
  const next = { ...settings };
1051
1133
  if (Object.keys(rest).length > 0) next.mcpServers = rest;
1052
1134
  else delete next.mcpServers;
1053
- if (Object.keys(next).length === 0) rmSync4(p.config);
1054
- else writeFileSync4(p.config, `${JSON.stringify(next, null, 2)}
1135
+ if (Object.keys(next).length === 0) rmSync5(p.config);
1136
+ else writeFileSync5(p.config, `${JSON.stringify(next, null, 2)}
1055
1137
  `);
1056
1138
  }
1057
1139
  const what = hadBlock && hadMcp ? `the Pair block and ${SERVER} server` : hadBlock ? "the Pair block" : `the ${SERVER} server`;
@@ -1062,9 +1144,9 @@ var VSCODE_FLAVOURS = ["Code", "Code - Insiders"];
1062
1144
  function vscodeUserDirs(o) {
1063
1145
  const home = o.home ?? homedir5();
1064
1146
  const platform2 = o.platform ?? process.platform;
1065
- const root = platform2 === "darwin" ? join7(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join7(home, "AppData", "Roaming") : join7(home, ".config");
1066
- const all = VSCODE_FLAVOURS.map((f) => join7(root, f, "User"));
1067
- const present = all.filter((d) => existsSync5(d));
1147
+ const root = platform2 === "darwin" ? join8(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join8(home, "AppData", "Roaming") : join8(home, ".config");
1148
+ const all = VSCODE_FLAVOURS.map((f) => join8(root, f, "User"));
1149
+ const present = all.filter((d) => existsSync6(d));
1068
1150
  return present.length > 0 ? present : [all[0]];
1069
1151
  }
1070
1152
  var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
@@ -1101,8 +1183,8 @@ function removeCopilotInstructions(path, dryRun) {
1101
1183
  if (before === void 0 || !hasPairBlock(before)) return false;
1102
1184
  if (dryRun) return true;
1103
1185
  const after = removePairBlock(before);
1104
- if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync4(path);
1105
- else writeFileSync4(path, after);
1186
+ if (after.trim() === "" || after.trim() === COPILOT_FRONTMATTER.trim()) rmSync5(path);
1187
+ else writeFileSync5(path, after);
1106
1188
  return true;
1107
1189
  }
1108
1190
  var copilotPair = {
@@ -1111,8 +1193,8 @@ var copilotPair = {
1111
1193
  installPair(o) {
1112
1194
  const lines = [];
1113
1195
  for (const dir of vscodeUserDirs(o)) {
1114
- const mcpPath = join7(dir, "mcp.json");
1115
- const instrPath = join7(dir, "prompts", "crew-pair.instructions.md");
1196
+ const mcpPath = join8(dir, "mcp.json");
1197
+ const instrPath = join8(dir, "prompts", "crew-pair.instructions.md");
1116
1198
  const settings = readVscodeMcp(mcpPath);
1117
1199
  if (!settings) {
1118
1200
  lines.push(`pair(copilot): ${mcpPath} is not a plain JSON object (comments are not supported) \u2014 fix it by hand; nothing was written there`);
@@ -1140,8 +1222,8 @@ var copilotPair = {
1140
1222
  },
1141
1223
  verifyPair(o) {
1142
1224
  const verdicts = vscodeUserDirs(o).map((dir) => {
1143
- const instrPath = join7(dir, "prompts", "crew-pair.instructions.md");
1144
- const mcpPath = join7(dir, "mcp.json");
1225
+ const instrPath = join8(dir, "prompts", "crew-pair.instructions.md");
1226
+ const mcpPath = join8(dir, "mcp.json");
1145
1227
  const b = blockVerdict(instrPath, "copilot");
1146
1228
  if (!b.ok) return b;
1147
1229
  const settings = readVscodeMcp(mcpPath);
@@ -1156,8 +1238,8 @@ var copilotPair = {
1156
1238
  removePair(o) {
1157
1239
  const lines = [];
1158
1240
  for (const dir of vscodeUserDirs(o)) {
1159
- const mcpPath = join7(dir, "mcp.json");
1160
- const hadBlock = removeCopilotInstructions(join7(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
1241
+ const mcpPath = join8(dir, "mcp.json");
1242
+ const hadBlock = removeCopilotInstructions(join8(dir, "prompts", "crew-pair.instructions.md"), o.dryRun);
1161
1243
  const settings = readVscodeMcp(mcpPath);
1162
1244
  const st = settings ? copilotMcpState(settings, o.url) : { state: "missing" };
1163
1245
  const hadMcp = st.state === "registered";
@@ -1167,8 +1249,8 @@ var copilotPair = {
1167
1249
  const next = { ...settings };
1168
1250
  if (Object.keys(rest).length > 0) next.servers = rest;
1169
1251
  else delete next.servers;
1170
- if (Object.keys(next).length === 0) rmSync4(mcpPath);
1171
- else writeFileSync4(mcpPath, `${JSON.stringify(next, null, 2)}
1252
+ if (Object.keys(next).length === 0) rmSync5(mcpPath);
1253
+ else writeFileSync5(mcpPath, `${JSON.stringify(next, null, 2)}
1172
1254
  `);
1173
1255
  }
1174
1256
  const what = hadBlock && hadMcp ? `the Pair instructions and ${SERVER} server` : hadBlock ? "the Pair instructions" : `the ${SERVER} server`;
@@ -1456,8 +1538,8 @@ function explain(ship, required) {
1456
1538
  }
1457
1539
 
1458
1540
  // src/repo-config.ts
1459
- import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
1460
- import { join as join8, basename as basename2 } from "path";
1541
+ import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
1542
+ import { join as join9, basename as basename2 } from "path";
1461
1543
  import { parse } from "yaml";
1462
1544
  var SPEC_VERSION = 1;
1463
1545
  var REPO_CONFIG_FILENAMES = [".crew.yaml", ".crew.yml", ".crew.json"];
@@ -1541,8 +1623,8 @@ var MODES = ["local", "integrate", "ci_manual", "ci_auto", "external"];
1541
1623
  var PROVIDERS = ["github", "buildkite", "other", "none"];
1542
1624
  function findRepoConfig(dir) {
1543
1625
  for (const name of REPO_CONFIG_FILENAMES) {
1544
- const p = join8(dir, name);
1545
- if (existsSync6(p)) return p;
1626
+ const p = join9(dir, name);
1627
+ if (existsSync7(p)) return p;
1546
1628
  }
1547
1629
  return null;
1548
1630
  }
@@ -1780,7 +1862,7 @@ function parseRepoConfig(text, file) {
1780
1862
  function loadRepoConfig(dir) {
1781
1863
  const file = findRepoConfig(dir);
1782
1864
  if (!file) return null;
1783
- return parseRepoConfig(readFileSync7(file, "utf8"), file);
1865
+ return parseRepoConfig(readFileSync8(file, "utf8"), file);
1784
1866
  }
1785
1867
  function hookLabel(cfg, hook2) {
1786
1868
  return cfg.labels[hook2] ?? cfg.hooks[hook2] ?? hook2;
@@ -2241,11 +2323,11 @@ function expand(p, base) {
2241
2323
  return isAbsolute(out) ? out : resolve(base, out);
2242
2324
  }
2243
2325
  function resolvePromptsDir(promptSet, crewHome, base) {
2244
- if (!promptSet) return join9(crewHome, "prompts", "default");
2326
+ if (!promptSet) return join10(crewHome, "prompts", "default");
2245
2327
  if (promptSet.includes("/") || promptSet.startsWith(".") || promptSet.startsWith("~")) {
2246
2328
  return expand(promptSet, base);
2247
2329
  }
2248
- return join9(crewHome, "prompts", promptSet);
2330
+ return join10(crewHome, "prompts", promptSet);
2249
2331
  }
2250
2332
  var Missing = class {
2251
2333
  paths = [];
@@ -2269,7 +2351,7 @@ var Missing = class {
2269
2351
  };
2270
2352
  function crewVersion(crewHome) {
2271
2353
  try {
2272
- const pkg = JSON.parse(readFileSync8(join9(crewHome, "package.json"), "utf8"));
2354
+ const pkg = JSON.parse(readFileSync9(join10(crewHome, "package.json"), "utf8"));
2273
2355
  return pkg.version ?? "0.0.0";
2274
2356
  } catch {
2275
2357
  return "0.0.0";
@@ -2279,26 +2361,26 @@ function configSearchPath(crewHome) {
2279
2361
  if (process.env.CREW_CONFIG) return [resolve(process.env.CREW_CONFIG)];
2280
2362
  const xdg = process.env.XDG_CONFIG_HOME;
2281
2363
  return [
2282
- ...xdg ? [join9(xdg, "crew", "crew.yaml")] : [],
2283
- join9(homedir6(), ".config", "crew", "crew.yaml"),
2364
+ ...xdg ? [join10(xdg, "crew", "crew.yaml")] : [],
2365
+ join10(homedir6(), ".config", "crew", "crew.yaml"),
2284
2366
  resolve(crewHome, "crew.yaml")
2285
2367
  ];
2286
2368
  }
2287
2369
  function findConfigFile(crewHome) {
2288
2370
  const candidates = configSearchPath(crewHome);
2289
- return candidates.find((c) => existsSync7(c)) ?? candidates[candidates.length - 1];
2371
+ return candidates.find((c) => existsSync8(c)) ?? candidates[candidates.length - 1];
2290
2372
  }
2291
- var defaultConfigPath = () => join9(process.env.XDG_CONFIG_HOME ?? join9(homedir6(), ".config"), "crew", "crew.yaml");
2373
+ var defaultConfigPath = () => join10(process.env.XDG_CONFIG_HOME ?? join10(homedir6(), ".config"), "crew", "crew.yaml");
2292
2374
  function routeSlug(route) {
2293
2375
  return route.replace(/\//g, "-");
2294
2376
  }
2295
2377
  function resolvedPathFor(stateDir, route) {
2296
2378
  const [workspace, project] = splitRoute(route);
2297
- return join9(stateDir, "resolved", workspace, `${project}.json`);
2379
+ return join10(stateDir, "resolved", workspace, `${project}.json`);
2298
2380
  }
2299
2381
  function apiKeyPathFor(stateDir, route) {
2300
2382
  const [workspace, project] = splitRoute(route);
2301
- return join9(stateDir, "keys", workspace, `${project}.env`);
2383
+ return join10(stateDir, "keys", workspace, `${project}.env`);
2302
2384
  }
2303
2385
  function splitRoute(route) {
2304
2386
  const parts = route.split("/");
@@ -2309,9 +2391,9 @@ function splitRoute(route) {
2309
2391
  }
2310
2392
  function readResolvedFile(stateDir, route) {
2311
2393
  const path = resolvedPathFor(stateDir, route);
2312
- if (!existsSync7(path)) return void 0;
2394
+ if (!existsSync8(path)) return void 0;
2313
2395
  try {
2314
- return JSON.parse(readFileSync8(path, "utf8"));
2396
+ return JSON.parse(readFileSync9(path, "utf8"));
2315
2397
  } catch (e) {
2316
2398
  throw new ConfigError(`${path}: not valid JSON \u2014 ${e.message}`);
2317
2399
  }
@@ -2371,18 +2453,18 @@ function parseResolved(raw, m, where) {
2371
2453
  }
2372
2454
  function loadConfig(crewHome, configFile, opts = {}) {
2373
2455
  const file = configFile ?? findConfigFile(crewHome);
2374
- if (opts.text === void 0 && !existsSync7(file)) {
2456
+ if (opts.text === void 0 && !existsSync8(file)) {
2375
2457
  throw new ConfigError(
2376
2458
  `no config found. Looked in:
2377
2459
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
2378
2460
 
2379
2461
  Create the first of those:
2380
- mkdir -p ${dirname3(defaultConfigPath())} && cp ${join9(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2462
+ mkdir -p ${dirname3(defaultConfigPath())} && cp ${join10(crewHome, "crew.example.yaml")} ${defaultConfigPath()}`
2381
2463
  );
2382
2464
  }
2383
2465
  let raw;
2384
2466
  try {
2385
- raw = parse2(opts.text ?? readFileSync8(file, "utf8"));
2467
+ raw = parse2(opts.text ?? readFileSync9(file, "utf8"));
2386
2468
  } catch (e) {
2387
2469
  throw new ConfigError(`${file}: not valid YAML \u2014 ${e.message}`);
2388
2470
  }
@@ -2492,7 +2574,7 @@ Create the first of those:
2492
2574
  useNvm: shipRaw.useNvm !== false,
2493
2575
  nvmSh: shipRaw.nvmSh,
2494
2576
  stateDir,
2495
- logFile: expand(shipRaw.logFile ?? join9(tmpdir(), "crew.log"), base),
2577
+ logFile: expand(shipRaw.logFile ?? join10(tmpdir(), "crew.log"), base),
2496
2578
  userAgent: shipRaw.userAgent ?? `Mozilla/5.0 CrewAgent/${crewVersion(crewHome)}`,
2497
2579
  maxConcurrentAgents,
2498
2580
  maxConcurrentAgentsSet,
@@ -2652,7 +2734,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2652
2734
  const hasRepos = Object.keys(repoDirs).length > 0;
2653
2735
  const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
2654
2736
  const dirIsCloneBase = !c?.dir && !hasRepos && !Array.isArray(c?.repos);
2655
- const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join9(reposBasePath, splitRoute(route)[0]) : "";
2737
+ const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join10(reposBasePath, splitRoute(route)[0]) : "";
2656
2738
  const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
2657
2739
  if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
2658
2740
  missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
@@ -2736,8 +2818,8 @@ function repoIdForName(r, name) {
2736
2818
  }
2737
2819
  function resolveApiKey(r) {
2738
2820
  if (r.apiKey) return r.apiKey;
2739
- if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
2740
- for (const line of readFileSync8(r.apiKeyFile, "utf8").split("\n")) {
2821
+ if (r.apiKeyFile && r.apiKeyVar && existsSync8(r.apiKeyFile)) {
2822
+ for (const line of readFileSync9(r.apiKeyFile, "utf8").split("\n")) {
2741
2823
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
2742
2824
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
2743
2825
  }
@@ -2750,9 +2832,9 @@ function resolveApiKey(r) {
2750
2832
  }
2751
2833
  function credentialSource(r) {
2752
2834
  if (r.apiKey) return KEYCHAIN_ELIGIBLE_ROUTES.has(r) ? "keychain" : "inline";
2753
- if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
2835
+ if (r.apiKeyFile && r.apiKeyVar && existsSync8(r.apiKeyFile)) {
2754
2836
  const re = new RegExp(`^${r.apiKeyVar}=`);
2755
- if (readFileSync8(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
2837
+ if (readFileSync9(r.apiKeyFile, "utf8").split("\n").some((l) => re.test(l))) return "apiKeyFile";
2756
2838
  }
2757
2839
  return process.env.CREW_API_KEY?.trim() ? "CREW_API_KEY" : "none";
2758
2840
  }
@@ -2808,7 +2890,7 @@ function configuredMembers(rt) {
2808
2890
  return out;
2809
2891
  }
2810
2892
  function defaultRepoDir(basePath, workspace, repoName) {
2811
- return join9(basePath, workspace, repoName);
2893
+ return join10(basePath, workspace, repoName);
2812
2894
  }
2813
2895
  function reposOf(r) {
2814
2896
  const named = Object.entries(r.repos);
@@ -2841,7 +2923,7 @@ function passengerRepoTargets(r) {
2841
2923
  return reposOf(r).filter((t) => r.repoOverrides[t.name]?.hostPassengers !== false);
2842
2924
  }
2843
2925
  function passengerCheckoutDir(basePath, workspace, repoName) {
2844
- return join9(basePath, workspace, ".passenger-checkouts", repoName);
2926
+ return join10(basePath, workspace, ".passenger-checkouts", repoName);
2845
2927
  }
2846
2928
  function passengerCheckoutTargets(r) {
2847
2929
  if (!r.hostPassengers) return [];
@@ -3128,9 +3210,9 @@ function recoveredFailureHeadlines(o, route) {
3128
3210
  }
3129
3211
 
3130
3212
  // src/install.ts
3131
- import { dirname as dirname4, join as join10, extname } from "path";
3213
+ import { dirname as dirname4, join as join11, extname } from "path";
3132
3214
  import { homedir as homedir7 } from "os";
3133
- import { accessSync, constants, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync9, statSync as statSync2, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
3215
+ import { accessSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync2, writeFileSync as writeFileSync6, unlinkSync, realpathSync } from "fs";
3134
3216
  import { spawnSync as spawnSync4 } from "child_process";
3135
3217
  import { createHash as createHash2 } from "crypto";
3136
3218
 
@@ -3142,7 +3224,7 @@ function isCompiledBinary(moduleUrl) {
3142
3224
  // src/install.ts
3143
3225
  function crewInvocation(crewHome) {
3144
3226
  if (isCompiledBinary(import.meta.url)) return [process.execPath];
3145
- return [process.execPath, join10(crewHome, "bin", "crew")];
3227
+ return [process.execPath, join11(crewHome, "bin", "crew")];
3146
3228
  }
3147
3229
  var INTERVAL_SECONDS = 120;
3148
3230
  function labelFor(crewHome, job) {
@@ -3180,7 +3262,7 @@ function pathFor(ship) {
3180
3262
  return parts.join(":");
3181
3263
  }
3182
3264
  function findOnPath(bin, searchPath) {
3183
- const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join10(dir, bin));
3265
+ const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join11(dir, bin));
3184
3266
  for (const candidate of candidates) {
3185
3267
  try {
3186
3268
  accessSync(candidate, constants.X_OK);
@@ -3190,7 +3272,7 @@ function findOnPath(bin, searchPath) {
3190
3272
  }
3191
3273
  return void 0;
3192
3274
  }
3193
- var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join10(homedir7(), ".docker", "bin")].join(":");
3275
+ var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join11(homedir7(), ".docker", "bin")].join(":");
3194
3276
  function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
3195
3277
  const schedulerPath = pathFor(ship);
3196
3278
  if (findOnPath("docker", schedulerPath)) return void 0;
@@ -3202,7 +3284,7 @@ function xmlEscape(s) {
3202
3284
  }
3203
3285
  function planLaunchd(ship, crewHome, job) {
3204
3286
  const label = labelFor(crewHome, job);
3205
- const unitPath = join10(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3287
+ const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3206
3288
  const crewLog = ship.logFile;
3207
3289
  const schedulerLog = schedulerLogFor(crewLog, job);
3208
3290
  const persistent = job === "run";
@@ -3256,8 +3338,8 @@ ${scheduleKeys}
3256
3338
  }
3257
3339
  function planSystemd(ship, crewHome, job) {
3258
3340
  const label = labelFor(crewHome, job);
3259
- const unitDir = join10(homedir7(), ".config", "systemd", "user");
3260
- const servicePath = join10(unitDir, `${label}.service`);
3341
+ const unitDir = join11(homedir7(), ".config", "systemd", "user");
3342
+ const servicePath = join11(unitDir, `${label}.service`);
3261
3343
  const crewLog = ship.logFile;
3262
3344
  const schedulerLog = schedulerLogFor(crewLog, job);
3263
3345
  const description = job === "release" ? "Tablation crew \u2014 release" : job === "passengers" ? "Tablation crew \u2014 Host Passengers sync" : "Tablation crew \u2014 persistent supervisor loop";
@@ -3297,7 +3379,7 @@ WantedBy=default.target
3297
3379
  schedulerLog
3298
3380
  };
3299
3381
  }
3300
- const timerPath = join10(unitDir, `${label}.timer`);
3382
+ const timerPath = join11(unitDir, `${label}.timer`);
3301
3383
  const service = `# Written by \`crew install\` \u2014 re-run it rather than hand-editing this file.
3302
3384
  [Unit]
3303
3385
  Description=${description}
@@ -3371,7 +3453,7 @@ function planInstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3371
3453
  function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3372
3454
  const label = labelFor(crewHome, job);
3373
3455
  if (hostShip === "macos") {
3374
- const unitPath = join10(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3456
+ const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3375
3457
  return {
3376
3458
  mechanism: "launchd",
3377
3459
  unitPaths: [unitPath],
@@ -3381,18 +3463,18 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3381
3463
  };
3382
3464
  }
3383
3465
  if (hostShip === "linux" && hasSystemd) {
3384
- const unitDir = join10(homedir7(), ".config", "systemd", "user");
3466
+ const unitDir = join11(homedir7(), ".config", "systemd", "user");
3385
3467
  if (job === "run") {
3386
3468
  return {
3387
3469
  mechanism: "systemd",
3388
- unitPaths: [join10(unitDir, `${label}.service`)],
3470
+ unitPaths: [join11(unitDir, `${label}.service`)],
3389
3471
  crontabLine: null,
3390
3472
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.service`]]
3391
3473
  };
3392
3474
  }
3393
3475
  return {
3394
3476
  mechanism: "systemd",
3395
- unitPaths: [join10(unitDir, `${label}.service`), join10(unitDir, `${label}.timer`)],
3477
+ unitPaths: [join11(unitDir, `${label}.service`), join11(unitDir, `${label}.timer`)],
3396
3478
  crontabLine: null,
3397
3479
  unloadCommands: [["systemctl", "--user", "disable", "--now", `${label}.timer`]]
3398
3480
  };
@@ -3410,7 +3492,7 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
3410
3492
  function planDaemonControl(crewHome, hostShip, hasSystemd) {
3411
3493
  const label = labelFor(crewHome, "run");
3412
3494
  if (hostShip === "macos") {
3413
- const unitPath = join10(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3495
+ const unitPath = join11(homedir7(), "Library", "LaunchAgents", `${label}.plist`);
3414
3496
  return {
3415
3497
  mechanism: "launchd",
3416
3498
  startCommand: ["launchctl", "load", unitPath],
@@ -3450,8 +3532,8 @@ async function applyInstall(plan, crewHome, dryRun, log) {
3450
3532
  log.emit(`would write ${path}`);
3451
3533
  continue;
3452
3534
  }
3453
- mkdirSync4(dirname4(path), { recursive: true });
3454
- writeFileSync5(path, plan.unitContent[path]);
3535
+ mkdirSync5(dirname4(path), { recursive: true });
3536
+ writeFileSync6(path, plan.unitContent[path]);
3455
3537
  log.emit(`wrote ${path}`);
3456
3538
  }
3457
3539
  if (plan.crontabLine) {
@@ -3505,7 +3587,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3505
3587
  }
3506
3588
  }
3507
3589
  for (const path of plan.unitPaths) {
3508
- if (!existsSync8(path)) continue;
3590
+ if (!existsSync9(path)) continue;
3509
3591
  if (dryRun) {
3510
3592
  log.emit(`would remove ${path}`);
3511
3593
  continue;
@@ -3524,7 +3606,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3524
3606
  }
3525
3607
  var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
3526
3608
  function launchAgentsDir() {
3527
- return join10(homedir7(), "Library", "LaunchAgents");
3609
+ return join11(homedir7(), "Library", "LaunchAgents");
3528
3610
  }
3529
3611
  function xmlUnescape(s) {
3530
3612
  return s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
@@ -3541,25 +3623,25 @@ function loadedCrewLabels() {
3541
3623
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3542
3624
  }
3543
3625
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3544
- const files = existsSync8(dir) ? readdirSync2(dir) : [];
3626
+ const files = existsSync9(dir) ? readdirSync3(dir) : [];
3545
3627
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3546
- const plistPath = join10(dir, f);
3628
+ const plistPath = join11(dir, f);
3547
3629
  let program = null;
3548
3630
  try {
3549
- program = programFromPlist(readFileSync9(plistPath, "utf8"));
3631
+ program = programFromPlist(readFileSync10(plistPath, "utf8"));
3550
3632
  } catch {
3551
3633
  }
3552
3634
  return {
3553
3635
  label: f.slice(0, -".plist".length),
3554
3636
  plistPath,
3555
3637
  program,
3556
- programExists: program !== null && existsSync8(program)
3638
+ programExists: program !== null && existsSync9(program)
3557
3639
  };
3558
3640
  });
3559
3641
  const seen = new Set(units.map((u) => u.label));
3560
3642
  for (const label of [...loaded].sort()) {
3561
3643
  if (!label.startsWith(CREW_LAUNCHD_PREFIX) || seen.has(label)) continue;
3562
- units.push({ label, plistPath: join10(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3644
+ units.push({ label, plistPath: join11(dir, `${label}.plist`), program: null, programExists: false, orphan: true });
3563
3645
  }
3564
3646
  return units;
3565
3647
  }
@@ -3572,9 +3654,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3572
3654
  log.emit(`would unload and remove ${unit.label}`);
3573
3655
  return;
3574
3656
  }
3575
- if (existsSync8(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3657
+ if (existsSync9(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3576
3658
  spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
3577
- if (existsSync8(unit.plistPath)) unlinkSync(unit.plistPath);
3659
+ if (existsSync9(unit.plistPath)) unlinkSync(unit.plistPath);
3578
3660
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3579
3661
  }
3580
3662
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -3652,24 +3734,24 @@ function detectSystemd() {
3652
3734
  }
3653
3735
 
3654
3736
  // src/ship-attention.ts
3655
- import { existsSync as existsSync10, statfsSync } from "fs";
3737
+ import { existsSync as existsSync11, statfsSync } from "fs";
3656
3738
 
3657
3739
  // src/git.ts
3658
3740
  import { execFileSync } from "child_process";
3659
3741
  import {
3660
- existsSync as existsSync9,
3661
- mkdirSync as mkdirSync5,
3742
+ existsSync as existsSync10,
3743
+ mkdirSync as mkdirSync6,
3662
3744
  openSync as openSync2,
3663
3745
  closeSync as closeSync2,
3664
3746
  writeSync,
3665
3747
  statSync as statSync3,
3666
3748
  unlinkSync as unlinkSync2,
3667
- readFileSync as readFileSync10,
3668
- writeFileSync as writeFileSync6,
3669
- readdirSync as readdirSync3,
3670
- rmSync as rmSync5
3749
+ readFileSync as readFileSync11,
3750
+ writeFileSync as writeFileSync7,
3751
+ readdirSync as readdirSync4,
3752
+ rmSync as rmSync6
3671
3753
  } from "fs";
3672
- import { dirname as dirname5, basename as basename4, join as join11 } from "path";
3754
+ import { dirname as dirname5, basename as basename4, join as join12 } from "path";
3673
3755
  var GitError = class extends Error {
3674
3756
  };
3675
3757
  function streamText(x) {
@@ -4050,7 +4132,7 @@ function recoverStrandedReleaseCheckouts(cwd) {
4050
4132
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
4051
4133
  gitOk(cwd, ["worktree", "remove", "--force", path]);
4052
4134
  const root = dirname5(path);
4053
- if (basename4(root).startsWith("crew-release-")) rmSync5(root, { recursive: true, force: true });
4135
+ if (basename4(root).startsWith("crew-release-")) rmSync6(root, { recursive: true, force: true });
4054
4136
  }
4055
4137
  }
4056
4138
  const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
@@ -4061,18 +4143,18 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
4061
4143
  const removed = [];
4062
4144
  let names;
4063
4145
  try {
4064
- names = readdirSync3(tmp);
4146
+ names = readdirSync4(tmp);
4065
4147
  } catch {
4066
4148
  return removed;
4067
4149
  }
4068
4150
  for (const name of names) {
4069
4151
  if (!name.startsWith("crew-release-")) continue;
4070
- const root = join11(tmp, name);
4152
+ const root = join12(tmp, name);
4071
4153
  try {
4072
4154
  const st = statSync3(root);
4073
4155
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
4074
- if (existsSync9(join11(root, "checkout"))) continue;
4075
- rmSync5(root, { recursive: true, force: true });
4156
+ if (existsSync10(join12(root, "checkout"))) continue;
4157
+ rmSync6(root, { recursive: true, force: true });
4076
4158
  removed.push(name);
4077
4159
  } catch {
4078
4160
  }
@@ -4117,13 +4199,13 @@ function originUrlOf(dir, remoteName) {
4117
4199
  }
4118
4200
  }
4119
4201
  function ensureRepoCheckout(dir, remote, opts = {}) {
4120
- if (existsSync9(`${dir}/.git`)) return false;
4202
+ if (existsSync10(`${dir}/.git`)) return false;
4121
4203
  if (!remote) {
4122
4204
  throw new GitError(
4123
4205
  `${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
4124
4206
  );
4125
4207
  }
4126
- mkdirSync5(dirname5(dir), { recursive: true });
4208
+ mkdirSync6(dirname5(dir), { recursive: true });
4127
4209
  const url = cloneUrlFor(remote, opts);
4128
4210
  try {
4129
4211
  execFileSync("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -4148,7 +4230,7 @@ function sleepSync(ms2) {
4148
4230
  }
4149
4231
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4150
4232
  const lockPath = checkoutLockPath(dir);
4151
- mkdirSync5(dirname5(lockPath), { recursive: true });
4233
+ mkdirSync6(dirname5(lockPath), { recursive: true });
4152
4234
  const deadline = Date.now() + timeoutMs;
4153
4235
  for (; ; ) {
4154
4236
  try {
@@ -4185,17 +4267,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4185
4267
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
4186
4268
  const release = acquireCheckoutLock(dir);
4187
4269
  try {
4188
- if (!existsSync9(`${dir}/.git`) && githubRemote) {
4270
+ if (!existsSync10(`${dir}/.git`) && githubRemote) {
4189
4271
  const url = cloneUrlFor(githubRemote, clone);
4190
4272
  const marker = cloneFailureMarker(dir);
4191
- if (existsSync9(marker) && readFileSync10(marker, "utf8").split("\n")[0] === url) {
4192
- return { action: "clone-failed", detail: readFileSync10(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4273
+ if (existsSync10(marker) && readFileSync11(marker, "utf8").split("\n")[0] === url) {
4274
+ return { action: "clone-failed", detail: readFileSync11(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4193
4275
  }
4194
4276
  try {
4195
4277
  ensureRepoCheckout(dir, githubRemote, clone);
4196
4278
  } catch (e) {
4197
4279
  if (!(e instanceof GitError)) throw e;
4198
- writeFileSync6(marker, `${url}
4280
+ writeFileSync7(marker, `${url}
4199
4281
  ${e.message}`);
4200
4282
  return { action: "clone-failed", detail: e.message, repeat: false };
4201
4283
  }
@@ -4402,7 +4484,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
4402
4484
  function reconcileHeldWorktrees(state, repo, log, stillRefused) {
4403
4485
  for (const i of state.shipAttention().previous()) {
4404
4486
  if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
4405
- const gone = !existsSync10(i.held.path);
4487
+ const gone = !existsSync11(i.held.path);
4406
4488
  if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
4407
4489
  clearShipAttention(state, i.key);
4408
4490
  log.emit(
@@ -4630,30 +4712,30 @@ function roleCapWarnings(ship) {
4630
4712
 
4631
4713
  // src/selftest.ts
4632
4714
  import { spawn as spawn3 } from "child_process";
4633
- import { existsSync as existsSync19, mkdirSync as mkdirSync10, mkdtempSync, readFileSync as readFileSync17, rmSync as rmSync9, writeFileSync as writeFileSync11 } from "fs";
4715
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, mkdtempSync, readFileSync as readFileSync18, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "fs";
4634
4716
  import { tmpdir as tmpdir2 } from "os";
4635
- import { join as join20 } from "path";
4717
+ import { join as join21 } from "path";
4636
4718
 
4637
4719
  // src/agent.ts
4638
4720
  import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
4639
- import { readFileSync as readFileSync16, existsSync as existsSync18, statSync as statSync5, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
4721
+ import { readFileSync as readFileSync17, existsSync as existsSync19, statSync as statSync5, mkdirSync as mkdirSync10, writeFileSync as writeFileSync11, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
4640
4722
  import { StringDecoder } from "string_decoder";
4641
4723
  import { finished } from "stream/promises";
4642
- import { join as join19, dirname as dirname7, basename as basename7 } from "path";
4724
+ import { join as join20, dirname as dirname7, basename as basename7 } from "path";
4643
4725
  import { createHash as createHash3 } from "crypto";
4644
4726
 
4645
4727
  // src/state.ts
4646
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync11, mkdirSync as mkdirSync6, rmSync as rmSync6, statSync as statSync4, readdirSync as readdirSync4 } from "fs";
4647
- import { join as join12 } from "path";
4728
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4, readdirSync as readdirSync5 } from "fs";
4729
+ import { join as join13 } from "path";
4648
4730
  var EPOCH = "1970-01-01T00:00:00.000Z";
4649
4731
  var State = class _State {
4650
4732
  dir;
4651
4733
  constructor(dir) {
4652
4734
  this.dir = dir;
4653
- mkdirSync6(dir, { recursive: true });
4735
+ mkdirSync7(dir, { recursive: true });
4654
4736
  }
4655
4737
  path(...parts) {
4656
- return join12(this.dir, ...parts);
4738
+ return join13(this.dir, ...parts);
4657
4739
  }
4658
4740
  /**
4659
4741
  * The timestamp every "new since last poll" check is measured against.
@@ -4664,14 +4746,14 @@ var State = class _State {
4664
4746
  */
4665
4747
  watermark() {
4666
4748
  try {
4667
- const v = readFileSync11(this.path(".poll-watermark"), "utf8").trim();
4749
+ const v = readFileSync12(this.path(".poll-watermark"), "utf8").trim();
4668
4750
  return v || EPOCH;
4669
4751
  } catch {
4670
4752
  return EPOCH;
4671
4753
  }
4672
4754
  }
4673
4755
  setWatermark(at) {
4674
- writeFileSync7(this.path(".poll-watermark"), `${at}
4756
+ writeFileSync8(this.path(".poll-watermark"), `${at}
4675
4757
  `);
4676
4758
  }
4677
4759
  /**
@@ -4689,19 +4771,19 @@ var State = class _State {
4689
4771
  }
4690
4772
  /** Whole-crew pause, and the per-role sentinels one level down. */
4691
4773
  isPaused() {
4692
- return existsSync11(this.path(".crew-paused"));
4774
+ return existsSync12(this.path(".crew-paused"));
4693
4775
  }
4694
4776
  isRolePaused(role) {
4695
- return existsSync11(this.path(`.role-paused-${role}`));
4777
+ return existsSync12(this.path(`.role-paused-${role}`));
4696
4778
  }
4697
4779
  pausedRoles(roles) {
4698
4780
  return new Set(roles.filter((r) => this.isRolePaused(r)));
4699
4781
  }
4700
4782
  pause(role) {
4701
- writeFileSync7(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
4783
+ writeFileSync8(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), "");
4702
4784
  }
4703
4785
  resume(role) {
4704
- rmSync6(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
4786
+ rmSync7(role ? this.path(`.role-paused-${role}`) : this.path(".crew-paused"), { force: true });
4705
4787
  }
4706
4788
  /**
4707
4789
  * The release phase (merge, deploy, release) has its own sentinel, one
@@ -4709,11 +4791,11 @@ var State = class _State {
4709
4791
  * while agents keep working.
4710
4792
  */
4711
4793
  isReleasePaused() {
4712
- return existsSync11(this.path(".release-paused"));
4794
+ return existsSync12(this.path(".release-paused"));
4713
4795
  }
4714
4796
  setReleasePaused(paused) {
4715
- if (paused) writeFileSync7(this.path(".release-paused"), "");
4716
- else rmSync6(this.path(".release-paused"), { force: true });
4797
+ if (paused) writeFileSync8(this.path(".release-paused"), "");
4798
+ else rmSync7(this.path(".release-paused"), { force: true });
4717
4799
  }
4718
4800
  /**
4719
4801
  * Why a release cycle must not run right now, or null when it may. Every
@@ -4750,7 +4832,7 @@ var State = class _State {
4750
4832
  const name = _State.safe(rawName);
4751
4833
  const file = this.path(`.${name}.lock`);
4752
4834
  try {
4753
- const existing = Number.parseInt(readFileSync11(file, "utf8").trim(), 10);
4835
+ const existing = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
4754
4836
  if (Number.isFinite(existing) && existing > 0) {
4755
4837
  try {
4756
4838
  process.kill(existing, 0);
@@ -4760,13 +4842,13 @@ var State = class _State {
4760
4842
  }
4761
4843
  } catch {
4762
4844
  }
4763
- writeFileSync7(file, `${process.pid}
4845
+ writeFileSync8(file, `${process.pid}
4764
4846
  `);
4765
4847
  return {
4766
4848
  ok: true,
4767
4849
  release: () => {
4768
4850
  try {
4769
- rmSync6(file, { force: true });
4851
+ rmSync7(file, { force: true });
4770
4852
  } catch {
4771
4853
  }
4772
4854
  }
@@ -4780,7 +4862,7 @@ var State = class _State {
4780
4862
  releaseInProgress() {
4781
4863
  let names;
4782
4864
  try {
4783
- names = readdirSync4(this.dir);
4865
+ names = readdirSync5(this.dir);
4784
4866
  } catch {
4785
4867
  return false;
4786
4868
  }
@@ -4788,7 +4870,7 @@ var State = class _State {
4788
4870
  if (!/^\.release-.+\.lock$/.test(n)) continue;
4789
4871
  let pid = 0;
4790
4872
  try {
4791
- pid = Number.parseInt(readFileSync11(this.path(n), "utf8").trim(), 10);
4873
+ pid = Number.parseInt(readFileSync12(this.path(n), "utf8").trim(), 10);
4792
4874
  } catch {
4793
4875
  continue;
4794
4876
  }
@@ -4874,7 +4956,7 @@ var State = class _State {
4874
4956
  ticketInFlight(recordId) {
4875
4957
  const file = this.path(`.${_State.safe(`crew-ticket-${recordId}`)}.lock`);
4876
4958
  try {
4877
- const pid = Number.parseInt(readFileSync11(file, "utf8").trim(), 10);
4959
+ const pid = Number.parseInt(readFileSync12(file, "utf8").trim(), 10);
4878
4960
  if (!Number.isFinite(pid) || pid <= 0) return false;
4879
4961
  process.kill(pid, 0);
4880
4962
  return true;
@@ -4908,7 +4990,7 @@ var State = class _State {
4908
4990
  const heldReported = `.release-held-${suffix}`;
4909
4991
  const read = (f) => {
4910
4992
  try {
4911
- return readFileSync11(this.path(f), "utf8").trim() || null;
4993
+ return readFileSync12(this.path(f), "utf8").trim() || null;
4912
4994
  } catch {
4913
4995
  return null;
4914
4996
  }
@@ -4917,12 +4999,12 @@ var State = class _State {
4917
4999
  blockedCount: () => Number.parseInt(read(blocked) ?? "", 10) || 0,
4918
5000
  noteBlocked: () => {
4919
5001
  const n = (Number.parseInt(read(blocked) ?? "", 10) || 0) + 1;
4920
- writeFileSync7(this.path(blocked), `${n}
5002
+ writeFileSync8(this.path(blocked), `${n}
4921
5003
  `);
4922
5004
  return n;
4923
5005
  },
4924
5006
  clearBlocked: () => {
4925
- rmSync6(this.path(blocked), { force: true });
5007
+ rmSync7(this.path(blocked), { force: true });
4926
5008
  },
4927
5009
  /**
4928
5010
  * The commit a deploy last failed on.
@@ -4934,11 +5016,11 @@ var State = class _State {
4934
5016
  */
4935
5017
  deployFailedSha: () => read(failed),
4936
5018
  noteDeployFailed: (sha) => {
4937
- writeFileSync7(this.path(failed), `${sha}
5019
+ writeFileSync8(this.path(failed), `${sha}
4938
5020
  `);
4939
5021
  },
4940
5022
  clearDeployFailed: () => {
4941
- rmSync6(this.path(failed), { force: true });
5023
+ rmSync7(this.path(failed), { force: true });
4942
5024
  },
4943
5025
  /**
4944
5026
  * Consecutive test-gate failures on the same head (CREW-1368). A failure
@@ -4948,31 +5030,31 @@ var State = class _State {
4948
5030
  noteTestGateFailed: (sha) => {
4949
5031
  const [prev, n] = (read(gateFailed) ?? "").split(" ");
4950
5032
  const next = prev === sha ? (Number.parseInt(n ?? "", 10) || 0) + 1 : 1;
4951
- writeFileSync7(this.path(gateFailed), `${sha} ${next}
5033
+ writeFileSync8(this.path(gateFailed), `${sha} ${next}
4952
5034
  `);
4953
5035
  return next;
4954
5036
  },
4955
5037
  clearTestGateFailed: () => {
4956
- rmSync6(this.path(gateFailed), { force: true });
5038
+ rmSync7(this.path(gateFailed), { force: true });
4957
5039
  },
4958
5040
  /** Whether the board was already told about this head's red gate. */
4959
5041
  heldReported: () => {
4960
5042
  try {
4961
- return readFileSync11(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
5043
+ return readFileSync12(this.path(heldReported), "utf8").replace(/\n$/, "") || null;
4962
5044
  } catch {
4963
5045
  return null;
4964
5046
  }
4965
5047
  },
4966
5048
  noteHeldReported: (signature) => {
4967
- writeFileSync7(this.path(heldReported), `${signature}
5049
+ writeFileSync8(this.path(heldReported), `${signature}
4968
5050
  `);
4969
5051
  },
4970
5052
  clearHeldReported: () => {
4971
- rmSync6(this.path(heldReported), { force: true });
5053
+ rmSync7(this.path(heldReported), { force: true });
4972
5054
  },
4973
5055
  testGateReported: (sha) => read(gateReported) === sha,
4974
5056
  noteTestGateReported: (sha) => {
4975
- writeFileSync7(this.path(gateReported), `${sha}
5057
+ writeFileSync8(this.path(gateReported), `${sha}
4976
5058
  `);
4977
5059
  }
4978
5060
  };
@@ -4995,13 +5077,13 @@ var State = class _State {
4995
5077
  const ticketStreakFile = `.fairness-ticket-streak-${suffix}`;
4996
5078
  const read = (f) => {
4997
5079
  try {
4998
- return readFileSync11(this.path(f), "utf8").trim() || null;
5080
+ return readFileSync12(this.path(f), "utf8").trim() || null;
4999
5081
  } catch {
5000
5082
  return null;
5001
5083
  }
5002
5084
  };
5003
5085
  const clear2 = () => {
5004
- for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync6(this.path(f), { force: true });
5086
+ for (const f of [streakFile, ticketFile, sinceFile, ticketStreakFile]) rmSync7(this.path(f), { force: true });
5005
5087
  };
5006
5088
  return {
5007
5089
  /** Consecutive cycles this route had actionable work and did not win. */
@@ -5028,18 +5110,18 @@ var State = class _State {
5028
5110
  clear2();
5029
5111
  return;
5030
5112
  }
5031
- writeFileSync7(this.path(streakFile), `${this.readInt(streakFile) + 1}
5113
+ writeFileSync8(this.path(streakFile), `${this.readInt(streakFile) + 1}
5032
5114
  `);
5033
5115
  const prior = read(ticketFile);
5034
5116
  if (topTicket && topTicket === prior) {
5035
- writeFileSync7(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5117
+ writeFileSync8(this.path(ticketStreakFile), `${this.readInt(ticketStreakFile) + 1}
5036
5118
  `);
5037
5119
  } else if (topTicket) {
5038
- writeFileSync7(this.path(ticketFile), `${topTicket}
5120
+ writeFileSync8(this.path(ticketFile), `${topTicket}
5039
5121
  `);
5040
- writeFileSync7(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5122
+ writeFileSync8(this.path(sinceFile), `${(/* @__PURE__ */ new Date()).toISOString()}
5041
5123
  `);
5042
- writeFileSync7(this.path(ticketStreakFile), "1\n");
5124
+ writeFileSync8(this.path(ticketStreakFile), "1\n");
5043
5125
  }
5044
5126
  }
5045
5127
  };
@@ -5060,14 +5142,14 @@ var State = class _State {
5060
5142
  return {
5061
5143
  previous: () => {
5062
5144
  try {
5063
- const parsed = JSON.parse(readFileSync11(this.path(file), "utf8"));
5145
+ const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5064
5146
  return parsed && typeof parsed === "object" ? parsed : {};
5065
5147
  } catch {
5066
5148
  return {};
5067
5149
  }
5068
5150
  },
5069
5151
  persist: (next) => {
5070
- writeFileSync7(this.path(file), JSON.stringify(next));
5152
+ writeFileSync8(this.path(file), JSON.stringify(next));
5071
5153
  }
5072
5154
  };
5073
5155
  }
@@ -5083,14 +5165,14 @@ var State = class _State {
5083
5165
  return {
5084
5166
  previous: () => {
5085
5167
  try {
5086
- const parsed = JSON.parse(readFileSync11(this.path(file), "utf8"));
5168
+ const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5087
5169
  return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string") : [];
5088
5170
  } catch {
5089
5171
  return [];
5090
5172
  }
5091
5173
  },
5092
5174
  persist: (next) => {
5093
- writeFileSync7(this.path(file), JSON.stringify(next));
5175
+ writeFileSync8(this.path(file), JSON.stringify(next));
5094
5176
  }
5095
5177
  };
5096
5178
  }
@@ -5104,14 +5186,14 @@ var State = class _State {
5104
5186
  return {
5105
5187
  previous: () => {
5106
5188
  try {
5107
- const parsed = JSON.parse(readFileSync11(this.path(file), "utf8"));
5189
+ const parsed = JSON.parse(readFileSync12(this.path(file), "utf8"));
5108
5190
  return Array.isArray(parsed) ? parsed : [];
5109
5191
  } catch {
5110
5192
  return [];
5111
5193
  }
5112
5194
  },
5113
5195
  persist: (next) => {
5114
- writeFileSync7(this.path(file), JSON.stringify(next));
5196
+ writeFileSync8(this.path(file), JSON.stringify(next));
5115
5197
  }
5116
5198
  };
5117
5199
  }
@@ -5124,7 +5206,7 @@ var State = class _State {
5124
5206
  const file = ".release-last-run";
5125
5207
  return {
5126
5208
  touch: () => {
5127
- writeFileSync7(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5209
+ writeFileSync8(this.path(file), (/* @__PURE__ */ new Date()).toISOString());
5128
5210
  },
5129
5211
  at: () => {
5130
5212
  try {
@@ -5137,7 +5219,7 @@ var State = class _State {
5137
5219
  }
5138
5220
  readInt(file) {
5139
5221
  try {
5140
- return Number.parseInt(readFileSync11(this.path(file), "utf8").trim(), 10) || 0;
5222
+ return Number.parseInt(readFileSync12(this.path(file), "utf8").trim(), 10) || 0;
5141
5223
  } catch {
5142
5224
  return 0;
5143
5225
  }
@@ -5620,8 +5702,8 @@ var PERISCOPE_COLUMNS = [
5620
5702
  ];
5621
5703
 
5622
5704
  // src/agent-log.ts
5623
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readdirSync as readdirSync5, readFileSync as readFileSync12, rmSync as rmSync7, writeFileSync as writeFileSync8 } from "fs";
5624
- import { join as join13 } from "path";
5705
+ import { existsSync as existsSync13, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
5706
+ import { join as join14 } from "path";
5625
5707
 
5626
5708
  // src/pending-backoff.ts
5627
5709
  var sleepBackoff = (attempts) => Math.min(6e4 * 2 ** Math.max(0, attempts - 1), 30 * 6e4);
@@ -5793,20 +5875,20 @@ async function addRunToTicket(target, t, recordId, run2) {
5793
5875
  };
5794
5876
  }
5795
5877
  }
5796
- var queueDir = (stateDir) => join13(stateDir, "pending-agent-logs");
5878
+ var queueDir = (stateDir) => join14(stateDir, "pending-agent-logs");
5797
5879
  function queueAgentRun(stateDir, name, q) {
5798
- mkdirSync7(queueDir(stateDir), { recursive: true });
5799
- writeFileSync8(join13(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
5880
+ mkdirSync8(queueDir(stateDir), { recursive: true });
5881
+ writeFileSync9(join14(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
5800
5882
  }
5801
5883
  async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
5802
5884
  const dir = queueDir(stateDir);
5803
- if (!existsSync12(dir)) return 0;
5885
+ if (!existsSync13(dir)) return 0;
5804
5886
  let sent = 0;
5805
- for (const f of readdirSync5(dir).filter((n) => n.endsWith(".json")).sort()) {
5806
- const path = join13(dir, f);
5887
+ for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
5888
+ const path = join14(dir, f);
5807
5889
  let q;
5808
5890
  try {
5809
- q = JSON.parse(readFileSync12(path, "utf8"));
5891
+ q = JSON.parse(readFileSync13(path, "utf8"));
5810
5892
  } catch {
5811
5893
  continue;
5812
5894
  }
@@ -5825,17 +5907,17 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
5825
5907
  attempts: 0,
5826
5908
  nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
5827
5909
  });
5828
- rmSync7(path, { force: true });
5910
+ rmSync8(path, { force: true });
5829
5911
  sent += 1;
5830
5912
  log.emit(`sent a queued Agent Log report (${f})`);
5831
5913
  } catch (e) {
5832
5914
  if (!(e instanceof AgentLogUnreachableError)) {
5833
- rmSync7(path, { force: true });
5915
+ rmSync8(path, { force: true });
5834
5916
  log.warn(`dropped a queued Agent Log report (${f}): ${e.message}`);
5835
5917
  continue;
5836
5918
  }
5837
5919
  const attempts = q.attempts + 1;
5838
- writeFileSync8(path, JSON.stringify({
5920
+ writeFileSync9(path, JSON.stringify({
5839
5921
  ...q,
5840
5922
  attempts,
5841
5923
  warned: true,
@@ -5846,20 +5928,20 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
5846
5928
  }
5847
5929
  return sent;
5848
5930
  }
5849
- var countersDir = (stateDir) => join13(stateDir, "pending-ticket-counters");
5931
+ var countersDir = (stateDir) => join14(stateDir, "pending-ticket-counters");
5850
5932
  function queueTicketCounters(stateDir, name, q) {
5851
- mkdirSync7(countersDir(stateDir), { recursive: true });
5852
- writeFileSync8(join13(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
5933
+ mkdirSync8(countersDir(stateDir), { recursive: true });
5934
+ writeFileSync9(join14(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
5853
5935
  }
5854
5936
  async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
5855
5937
  const dir = countersDir(stateDir);
5856
- if (!existsSync12(dir)) return 0;
5938
+ if (!existsSync13(dir)) return 0;
5857
5939
  let sent = 0;
5858
- for (const f of readdirSync5(dir).filter((n) => n.endsWith(".json")).sort()) {
5859
- const path = join13(dir, f);
5940
+ for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
5941
+ const path = join14(dir, f);
5860
5942
  let q;
5861
5943
  try {
5862
- q = JSON.parse(readFileSync12(path, "utf8"));
5944
+ q = JSON.parse(readFileSync13(path, "utf8"));
5863
5945
  } catch {
5864
5946
  continue;
5865
5947
  }
@@ -5868,17 +5950,17 @@ async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /*
5868
5950
  const apiKey = await keyFor(q.role);
5869
5951
  if (!apiKey) continue;
5870
5952
  await writeCounters({ ...q.target, apiKey }, q.write);
5871
- rmSync7(path, { force: true });
5953
+ rmSync8(path, { force: true });
5872
5954
  sent += 1;
5873
5955
  log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
5874
5956
  } catch (e) {
5875
5957
  if (!isRetryable(e)) {
5876
- rmSync7(path, { force: true });
5958
+ rmSync8(path, { force: true });
5877
5959
  log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
5878
5960
  continue;
5879
5961
  }
5880
5962
  const attempts = q.attempts + 1;
5881
- writeFileSync8(path, JSON.stringify({
5963
+ writeFileSync9(path, JSON.stringify({
5882
5964
  ...q,
5883
5965
  attempts,
5884
5966
  warned: true,
@@ -6081,8 +6163,8 @@ var HandoffTranscriptTracker = class {
6081
6163
  };
6082
6164
 
6083
6165
  // src/pending-handoff.ts
6084
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, readdirSync as readdirSync6, readFileSync as readFileSync13, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
6085
- import { join as join16 } from "path";
6166
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, readdirSync as readdirSync7, readFileSync as readFileSync14, rmSync as rmSync9, writeFileSync as writeFileSync10 } from "fs";
6167
+ import { join as join17 } from "path";
6086
6168
 
6087
6169
  // src/ticket-branch.ts
6088
6170
  import { basename as basename6 } from "path";
@@ -6905,6 +6987,33 @@ var Tracker = class {
6905
6987
  const { shipId } = await this.authorshipProbe();
6906
6988
  return this.client.records.create(modelId, { ...fields, ...shipId ? { ship_id: shipId } : {} });
6907
6989
  }
6990
+ /**
6991
+ * This ship's Periscopes rows (EPIC-077), newest first, with the ticket each
6992
+ * belongs to. Empty when there is no Periscopes table or no Ships row yet.
6993
+ */
6994
+ async periscopeRows(states) {
6995
+ const modelId = this.route.resolved?.periscopesModelId;
6996
+ const { shipId } = await this.authorshipProbe();
6997
+ if (!modelId || !shipId) return [];
6998
+ const conds = [{ columnName: "ship_id", operator: "EQ", value: shipId }];
6999
+ if (states?.length) conds.push({ columnName: "state", operator: "IN", value: states });
7000
+ const rows = await this.client.records.list(modelId, { filters: encodeFilters(conds), limit: 200 });
7001
+ const refId = (v) => typeof v === "string" ? v : v && typeof v === "object" && typeof v.id === "string" ? v.id : void 0;
7002
+ const ids = [...new Set(rows.map((r) => refId(r.issue_id)).filter((x) => !!x))];
7003
+ const tickets = new Map((await this.ticketsByIds(ids)).map((t) => [t.id, t]));
7004
+ const out = [];
7005
+ for (const row of rows) {
7006
+ const t = tickets.get(refId(row.issue_id) ?? "");
7007
+ if (t) out.push({ row, ticket: t });
7008
+ }
7009
+ return out.sort((a, b) => String(b.row.started_at ?? "").localeCompare(String(a.row.started_at ?? "")));
7010
+ }
7011
+ /** Updates one Periscopes row (EPIC-077). */
7012
+ async updatePeriscope(id, patch2) {
7013
+ const modelId = this.route.resolved?.periscopesModelId;
7014
+ if (!modelId) throw new Error("this workspace has no Periscopes table");
7015
+ await this.client.records.update(modelId, id, patch2);
7016
+ }
6908
7017
  /**
6909
7018
  * The authorship columns this workspace is missing, as `table.column` names
6910
7019
  * for `crew doctor` to report (CREW-1371). Empty on a fully updated install.
@@ -7125,13 +7234,13 @@ function holdWarnings(tickets, ships, contract, humanHolds, now = Date.now()) {
7125
7234
  }
7126
7235
 
7127
7236
  // src/run-worktree.ts
7128
- import { existsSync as existsSync14 } from "fs";
7129
- import { dirname as dirname6, join as join15 } from "path";
7237
+ import { existsSync as existsSync15 } from "fs";
7238
+ import { dirname as dirname6, join as join16 } from "path";
7130
7239
 
7131
7240
  // src/ports.ts
7132
7241
  import { execFileSync as execFileSync3 } from "child_process";
7133
- import { existsSync as existsSync13 } from "fs";
7134
- import { join as join14 } from "path";
7242
+ import { existsSync as existsSync14 } from "fs";
7243
+ import { join as join15 } from "path";
7135
7244
  var run = (cmd, args) => {
7136
7245
  try {
7137
7246
  return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -7176,7 +7285,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
7176
7285
  }
7177
7286
  function worktreeExists(parent, prefix, n) {
7178
7287
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
7179
- if (existsSync13(join14(parent, name, ".git"))) return true;
7288
+ if (existsSync14(join15(parent, name, ".git"))) return true;
7180
7289
  }
7181
7290
  return false;
7182
7291
  }
@@ -7259,7 +7368,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
7259
7368
  const existing = worktreeForNumber(dir, num);
7260
7369
  const located = locateBranchForTicket(dir, cfg, t, role);
7261
7370
  const baseRef = hasRemote ? `${remote}/${base}` : base;
7262
- if (existing && !existing.prunable && existsSync14(existing.path)) {
7371
+ if (existing && !existing.prunable && existsSync15(existing.path)) {
7263
7372
  const branch = existing.branch;
7264
7373
  if (status(existing.path).length > 0) {
7265
7374
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -7275,7 +7384,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
7275
7384
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
7276
7385
  return { action: "recut", path: existing.path, branch, base: baseRef };
7277
7386
  }
7278
- const path = join15(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
7387
+ const path = join16(dirname6(dir), worktreeForTicket(dir, cfg, t, role));
7279
7388
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
7280
7389
  return { action: "cut-remote", path, branch: located.branch };
7281
7390
  }
@@ -7329,7 +7438,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
7329
7438
  if (remoteConfigured(dir, remote) && remoteBranchExists(dir, remote, branch)) return false;
7330
7439
  if (countCommits(dir, `${baseRef}..${branch}`) !== 0) return false;
7331
7440
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7332
- if (wt && existsSync14(wt.path)) {
7441
+ if (wt && existsSync15(wt.path)) {
7333
7442
  if (status(wt.path).length > 0) return false;
7334
7443
  if (gitOk(dir, ["worktree", "remove", wt.path]) === null) return false;
7335
7444
  }
@@ -7337,7 +7446,7 @@ function dropEmptyRunBranch(dir, cfg, branch) {
7337
7446
  }
7338
7447
  async function finishRunWorktree(dir, cfg, branch) {
7339
7448
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
7340
- if (!wt || !existsSync14(wt.path)) return { kind: "none" };
7449
+ if (!wt || !existsSync15(wt.path)) return { kind: "none" };
7341
7450
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
7342
7451
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
7343
7452
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -7349,7 +7458,7 @@ async function finishRunWorktree(dir, cfg, branch) {
7349
7458
  if (pids.length) await killGently(pids);
7350
7459
  }
7351
7460
  gitOk(dir, ["worktree", "remove", wt.path]);
7352
- return existsSync14(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7461
+ return existsSync15(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
7353
7462
  }
7354
7463
 
7355
7464
  // src/handoff-guard.ts
@@ -7732,20 +7841,20 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
7732
7841
 
7733
7842
  // src/pending-handoff.ts
7734
7843
  var PENDING_MAX_AGE_MS = 48 * 36e5;
7735
- var pendingDirFor = (stateDir) => join16(stateDir, "pending-handoffs");
7736
- var stateDirOfStream = (streamPath) => join16(streamPath, "..", "..");
7844
+ var pendingDirFor = (stateDir) => join17(stateDir, "pending-handoffs");
7845
+ var stateDirOfStream = (streamPath) => join17(streamPath, "..", "..");
7737
7846
  function writePendingHandoff(stateDir, rec) {
7738
7847
  const dir = pendingDirFor(stateDir);
7739
- mkdirSync8(dir, { recursive: true });
7740
- writeFileSync9(join16(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
7848
+ mkdirSync9(dir, { recursive: true });
7849
+ writeFileSync10(join17(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
7741
7850
  }
7742
7851
  function listPendingHandoffs(stateDir, route) {
7743
7852
  const dir = pendingDirFor(stateDir);
7744
- if (!existsSync15(dir)) return [];
7853
+ if (!existsSync16(dir)) return [];
7745
7854
  const out = [];
7746
- for (const f of readdirSync6(dir).filter((n) => n.endsWith(".json")).sort()) {
7855
+ for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
7747
7856
  try {
7748
- const rec = JSON.parse(readFileSync13(join16(dir, f), "utf8"));
7857
+ const rec = JSON.parse(readFileSync14(join17(dir, f), "utf8"));
7749
7858
  if (rec.version === 1 && rec.ticketRecordId && (!route || rec.route === route)) out.push(rec);
7750
7859
  } catch {
7751
7860
  }
@@ -7753,7 +7862,7 @@ function listPendingHandoffs(stateDir, route) {
7753
7862
  return out;
7754
7863
  }
7755
7864
  function removePendingHandoff(stateDir, ticketRecordId) {
7756
- rmSync8(join16(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
7865
+ rmSync9(join17(pendingDirFor(stateDir), `${ticketRecordId}.json`), { force: true });
7757
7866
  }
7758
7867
  function pendingHandoffFor(stateDir, ticketRecordId) {
7759
7868
  return listPendingHandoffs(stateDir).find((r) => r.ticketRecordId === ticketRecordId);
@@ -7863,8 +7972,8 @@ function expandSkillMarkers(text, lookup2, opts = {}) {
7863
7972
  }
7864
7973
 
7865
7974
  // src/skills.ts
7866
- import { readFileSync as readFileSync15, readdirSync as readdirSync7, existsSync as existsSync17 } from "fs";
7867
- import { join as join18 } from "path";
7975
+ import { readFileSync as readFileSync16, readdirSync as readdirSync8, existsSync as existsSync18 } from "fs";
7976
+ import { join as join19 } from "path";
7868
7977
  import { parse as parseYaml } from "yaml";
7869
7978
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError3, TablationApiError as TablationApiError2 } from "@tablation/client";
7870
7979
 
@@ -7880,8 +7989,8 @@ ${additions}`;
7880
7989
  }
7881
7990
 
7882
7991
  // src/agents.ts
7883
- import { readFileSync as readFileSync14, existsSync as existsSync16 } from "fs";
7884
- import { join as join17 } from "path";
7992
+ import { readFileSync as readFileSync15, existsSync as existsSync17 } from "fs";
7993
+ import { join as join18 } from "path";
7885
7994
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError2, TablationApiError } from "@tablation/client";
7886
7995
  var PERSONA_NAME = {
7887
7996
  dev: "Developer",
@@ -7895,12 +8004,12 @@ var CREW_AGENT_COLUMN = "agent_id";
7895
8004
  var AgentsSyncError = class extends Error {
7896
8005
  };
7897
8006
  function personaDefaultPrompt(promptsDir, role) {
7898
- const briefPath = join17(promptsDir, "personas", `lane-${role}.md`);
7899
- if (!existsSync16(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7900
- if (SELF_CONTAINED_ROLES.has(role)) return readFileSync14(briefPath, "utf8");
7901
- const commonPath = join17(promptsDir, "personas", "common.md");
7902
- if (!existsSync16(commonPath)) return readFileSync14(briefPath, "utf8");
7903
- return `${readFileSync14(commonPath, "utf8")}${readFileSync14(briefPath, "utf8")}`;
8007
+ const briefPath = join18(promptsDir, "personas", `lane-${role}.md`);
8008
+ if (!existsSync17(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
8009
+ if (SELF_CONTAINED_ROLES.has(role)) return readFileSync15(briefPath, "utf8");
8010
+ const commonPath = join18(promptsDir, "personas", "common.md");
8011
+ if (!existsSync17(commonPath)) return readFileSync15(briefPath, "utf8");
8012
+ return `${readFileSync15(commonPath, "utf8")}${readFileSync15(briefPath, "utf8")}`;
7904
8013
  }
7905
8014
  async function resolveAgentId(route, role, opts) {
7906
8015
  const cached = route.resolved?.agentPersonas?.[role]?.agentId;
@@ -8347,7 +8456,7 @@ function describeCrewLink(o) {
8347
8456
  var SkillSyncError = class extends Error {
8348
8457
  };
8349
8458
  function parseSkillFile(path) {
8350
- const text = readFileSync15(path, "utf8");
8459
+ const text = readFileSync16(path, "utf8");
8351
8460
  const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
8352
8461
  if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
8353
8462
  const frontmatter = match[1];
@@ -8360,9 +8469,9 @@ function parseSkillFile(path) {
8360
8469
  return { name: meta.name, description: meta.description, prompt: body, file: path };
8361
8470
  }
8362
8471
  function listLocalSkills(promptsDir) {
8363
- const dir = join18(promptsDir, "skills");
8364
- if (!existsSync17(dir)) return [];
8365
- return readdirSync7(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join18(dir, f)));
8472
+ const dir = join19(promptsDir, "skills");
8473
+ if (!existsSync18(dir)) return [];
8474
+ return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join19(dir, f)));
8366
8475
  }
8367
8476
  async function syncSkills(route, opts) {
8368
8477
  if (!route.resolved) {
@@ -8564,26 +8673,26 @@ var AgentError = class extends Error {
8564
8673
  };
8565
8674
  var AgentSpawnError = class extends AgentError {
8566
8675
  };
8567
- var digestPathFor = (stateDir, routeLabel, role, runId) => join19(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
8676
+ var digestPathFor = (stateDir, routeLabel, role, runId) => join20(stateDir, `digest-${routeLabel}-${role}${runId ? `-${runId}` : ""}.md`);
8568
8677
  function planAgentRun(o) {
8569
- const briefPath = join19(o.route.promptsDir, "personas", `lane-${o.role}.md`);
8570
- if (!existsSync18(briefPath)) {
8678
+ const briefPath = join20(o.route.promptsDir, "personas", `lane-${o.role}.md`);
8679
+ if (!existsSync19(briefPath)) {
8571
8680
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
8572
8681
  }
8573
- const commonPath = join19(o.route.promptsDir, "personas", "common.md");
8574
- const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync18(commonPath);
8682
+ const commonPath = join20(o.route.promptsDir, "personas", "common.md");
8683
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync19(commonPath);
8575
8684
  const now = o.now ?? (() => Date.now());
8576
8685
  const routeLabel = routeSlug(o.route.route);
8577
8686
  const ownDigest = o.runId ? digestPathFor(o.stateDir, routeLabel, o.role, o.runId) : void 0;
8578
- const digestPath2 = ownDigest && existsSync18(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
8687
+ const digestPath2 = ownDigest && existsSync19(ownDigest) ? ownDigest : digestPathFor(o.stateDir, routeLabel, o.role);
8579
8688
  let digest;
8580
8689
  let ageSeconds;
8581
- if (existsSync18(digestPath2)) {
8690
+ if (existsSync19(digestPath2)) {
8582
8691
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
8583
- if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync16(digestPath2, "utf8");
8692
+ if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync17(digestPath2, "utf8");
8584
8693
  }
8585
- const common = usesCommon ? readFileSync16(commonPath, "utf8") : "";
8586
- const brief = readFileSync16(briefPath, "utf8");
8694
+ const common = usesCommon ? readFileSync17(commonPath, "utf8") : "";
8695
+ const brief = readFileSync17(briefPath, "utf8");
8587
8696
  const effectiveCommon = o.divergedPrompt ? "" : common;
8588
8697
  const effectiveBrief = appendAdditions(o.divergedPrompt ?? brief, o.personaAdditions);
8589
8698
  const localSkills = new Map(listLocalSkills(o.route.promptsDir).map((s) => [s.name, s.prompt]));
@@ -8605,7 +8714,7 @@ function planAgentRun(o) {
8605
8714
  digest
8606
8715
  });
8607
8716
  const promptSha = createHash3("sha256").update(expanded.text).digest("hex");
8608
- const streamsDir = join19(o.stateDir, "streams");
8717
+ const streamsDir = join20(o.stateDir, "streams");
8609
8718
  const base = `${routeLabel}-${o.role}-${o.cycle}${o.runId ? `-${o.runId}` : ""}`;
8610
8719
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
8611
8720
  const { toolchain, model } = seat;
@@ -8675,8 +8784,8 @@ function planAgentRun(o) {
8675
8784
  digestAttached: digest !== void 0,
8676
8785
  digestAgeSeconds: ageSeconds,
8677
8786
  model,
8678
- streamPath: join19(streamsDir, `${base}.jsonl`),
8679
- eventsPath: join19(streamsDir, `${base}.events.jsonl`),
8787
+ streamPath: join20(streamsDir, `${base}.jsonl`),
8788
+ eventsPath: join20(streamsDir, `${base}.events.jsonl`),
8680
8789
  ticket: o.ticket,
8681
8790
  ticketRecordId: o.ticketRecordId ?? null,
8682
8791
  repo: o.repo ?? null,
@@ -8707,7 +8816,7 @@ function describePlan(p) {
8707
8816
  var pidPathFor = (streamPath) => `${streamPath}.pid`;
8708
8817
  function readRunSidecar(streamPath) {
8709
8818
  try {
8710
- const sc = JSON.parse(readFileSync16(pidPathFor(streamPath), "utf8"));
8819
+ const sc = JSON.parse(readFileSync17(pidPathFor(streamPath), "utf8"));
8711
8820
  return typeof sc.pid === "number" && typeof sc.spawnedAt === "string" && typeof sc.role === "string" ? sc : null;
8712
8821
  } catch {
8713
8822
  return null;
@@ -8743,7 +8852,7 @@ function identityOf(plan, adapterHarness) {
8743
8852
  var withoutKey = ({ apiKey: _apiKey, ...rest }) => rest;
8744
8853
  function openSink(path) {
8745
8854
  try {
8746
- mkdirSync9(dirname7(path), { recursive: true });
8855
+ mkdirSync10(dirname7(path), { recursive: true });
8747
8856
  return createWriteStream(path, { flags: "a" });
8748
8857
  } catch {
8749
8858
  return void 0;
@@ -8925,7 +9034,7 @@ var RunObserver = class {
8925
9034
  };
8926
9035
  function writeRunMeta(id, startedAt, liveTicket, extra) {
8927
9036
  try {
8928
- writeFileSync10(`${id.streamPath}.meta.json`, JSON.stringify({
9037
+ writeFileSync11(`${id.streamPath}.meta.json`, JSON.stringify({
8929
9038
  route: id.route,
8930
9039
  role: id.role,
8931
9040
  hint: id.ticket,
@@ -9085,7 +9194,7 @@ function spawnAgent(plan, emit, hintMoved) {
9085
9194
  return new Promise((resolve6, reject) => {
9086
9195
  let streamFd;
9087
9196
  try {
9088
- mkdirSync9(dirname7(plan.streamPath), { recursive: true });
9197
+ mkdirSync10(dirname7(plan.streamPath), { recursive: true });
9089
9198
  streamFd = openSync3(plan.streamPath, "a");
9090
9199
  } catch (e) {
9091
9200
  void endSink(eventsSink);
@@ -9120,7 +9229,7 @@ function spawnAgent(plan, emit, hintMoved) {
9120
9229
  promptSha: plan.promptSha
9121
9230
  };
9122
9231
  try {
9123
- writeFileSync10(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
9232
+ writeFileSync11(pidPathFor(plan.streamPath), JSON.stringify(sidecar));
9124
9233
  } catch {
9125
9234
  }
9126
9235
  }
@@ -9623,7 +9732,7 @@ async function runSelftest(i, deps = realDeps) {
9623
9732
  ...adapter.runEnv(spec),
9624
9733
  [KEY_VAR]: i.apiKey
9625
9734
  };
9626
- const cwd = mkdtempSync(join20(tmpdir2(), "crew-selftest-"));
9735
+ const cwd = mkdtempSync(join21(tmpdir2(), "crew-selftest-"));
9627
9736
  try {
9628
9737
  const run2 = await deps.run({
9629
9738
  bin: invocation.bin,
@@ -9645,7 +9754,7 @@ async function runSelftest(i, deps = realDeps) {
9645
9754
  return finish({ ok: false, failure, detail, ...usage2 });
9646
9755
  } finally {
9647
9756
  try {
9648
- rmSync9(cwd, { recursive: true, force: true });
9757
+ rmSync10(cwd, { recursive: true, force: true });
9649
9758
  } catch {
9650
9759
  }
9651
9760
  }
@@ -9656,11 +9765,11 @@ function formatResult(r) {
9656
9765
  const secs = `${(r.ms / 1e3).toFixed(1)}s`;
9657
9766
  return r.ok ? `${r.toolchain}: PASS ${r.detail} (${secs}${tok})` : `${r.toolchain}: FAIL ${r.failure} \u2014 ${r.detail} (${secs})`;
9658
9767
  }
9659
- var fileFor2 = (stateDir) => join20(stateDir, "selftest.json");
9768
+ var fileFor2 = (stateDir) => join21(stateDir, "selftest.json");
9660
9769
  function readSelftest(stateDir) {
9661
9770
  try {
9662
9771
  const p = fileFor2(stateDir);
9663
- return existsSync19(p) ? JSON.parse(readFileSync17(p, "utf8")) : {};
9772
+ return existsSync20(p) ? JSON.parse(readFileSync18(p, "utf8")) : {};
9664
9773
  } catch {
9665
9774
  return {};
9666
9775
  }
@@ -9668,8 +9777,8 @@ function readSelftest(stateDir) {
9668
9777
  function recordSelftest(stateDir, results) {
9669
9778
  const all = readSelftest(stateDir);
9670
9779
  for (const r of results) all[r.toolchain] = r;
9671
- mkdirSync10(stateDir, { recursive: true });
9672
- writeFileSync11(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
9780
+ mkdirSync11(stateDir, { recursive: true });
9781
+ writeFileSync12(fileFor2(stateDir), `${JSON.stringify(all, null, 2)}
9673
9782
  `);
9674
9783
  }
9675
9784
  function doctorSelftestLines(stored, toolchains) {
@@ -9681,7 +9790,7 @@ function doctorSelftestLines(stored, toolchains) {
9681
9790
  }
9682
9791
 
9683
9792
  // src/cli.ts
9684
- import { resolve as resolve5, dirname as dirname20, join as join38, basename as basename12 } from "path";
9793
+ import { resolve as resolve5, dirname as dirname20, join as join39, basename as basename12 } from "path";
9685
9794
  import { fileURLToPath } from "url";
9686
9795
  import { hostname as hostname2, homedir as homedir10 } from "os";
9687
9796
  import { spawnSync as spawnSync7 } from "child_process";
@@ -9863,14 +9972,14 @@ function rosterMarkdown(roster, meId) {
9863
9972
  }
9864
9973
 
9865
9974
  // src/connect-resume.ts
9866
- import { mkdirSync as mkdirSync11, readFileSync as readFileSync18, rmSync as rmSync10, writeFileSync as writeFileSync12 } from "fs";
9867
- import { dirname as dirname8, join as join21 } from "path";
9975
+ import { mkdirSync as mkdirSync12, readFileSync as readFileSync19, rmSync as rmSync11, writeFileSync as writeFileSync13 } from "fs";
9976
+ import { dirname as dirname8, join as join22 } from "path";
9868
9977
  function lastSessionFile(stateDir) {
9869
- return join21(stateDir, "connect-session.json");
9978
+ return join22(stateDir, "connect-session.json");
9870
9979
  }
9871
9980
  function readPointers(stateDir) {
9872
9981
  try {
9873
- const parsed = JSON.parse(readFileSync18(lastSessionFile(stateDir), "utf8"));
9982
+ const parsed = JSON.parse(readFileSync19(lastSessionFile(stateDir), "utf8"));
9874
9983
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
9875
9984
  const out = {};
9876
9985
  for (const [host, v] of Object.entries(parsed)) {
@@ -9884,8 +9993,8 @@ function readPointers(stateDir) {
9884
9993
  }
9885
9994
  function writePointers(stateDir, pointers) {
9886
9995
  const file = lastSessionFile(stateDir);
9887
- mkdirSync11(dirname8(file), { recursive: true });
9888
- writeFileSync12(file, `${JSON.stringify(pointers, null, 2)}
9996
+ mkdirSync12(dirname8(file), { recursive: true });
9997
+ writeFileSync13(file, `${JSON.stringify(pointers, null, 2)}
9889
9998
  `, { mode: 384 });
9890
9999
  }
9891
10000
  function rememberSession(stateDir, host, workspaceSlug) {
@@ -9902,7 +10011,7 @@ function forgetWorkspace(stateDir, host, workspaceSlug) {
9902
10011
  const { [host]: _old, ...rest } = all;
9903
10012
  const next = left.length ? { ...rest, [host]: left } : rest;
9904
10013
  if (Object.keys(next).length) writePointers(stateDir, next);
9905
- else rmSync10(lastSessionFile(stateDir), { force: true });
10014
+ else rmSync11(lastSessionFile(stateDir), { force: true });
9906
10015
  } catch {
9907
10016
  }
9908
10017
  }
@@ -9942,7 +10051,7 @@ function forgetSession(stateDir, host) {
9942
10051
  try {
9943
10052
  const { [host]: _gone, ...rest } = readPointers(stateDir);
9944
10053
  if (Object.keys(rest).length) writePointers(stateDir, rest);
9945
- else rmSync10(lastSessionFile(stateDir), { force: true });
10054
+ else rmSync11(lastSessionFile(stateDir), { force: true });
9946
10055
  } catch {
9947
10056
  }
9948
10057
  }
@@ -9970,15 +10079,15 @@ async function signOutSessions(o) {
9970
10079
  }
9971
10080
 
9972
10081
  // src/reset.ts
9973
- import { existsSync as existsSync20, mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync8, rmSync as rmSync11, statSync as statSync6, writeFileSync as writeFileSync13 } from "fs";
9974
- import { dirname as dirname9, join as join22 } from "path";
10082
+ import { existsSync as existsSync21, mkdirSync as mkdirSync13, readFileSync as readFileSync20, readdirSync as readdirSync9, rmSync as rmSync12, statSync as statSync6, writeFileSync as writeFileSync14 } from "fs";
10083
+ import { dirname as dirname9, join as join23 } from "path";
9975
10084
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
9976
10085
  function clonedCheckoutsFile(stateDir) {
9977
- return join22(stateDir, "cloned-checkouts.json");
10086
+ return join23(stateDir, "cloned-checkouts.json");
9978
10087
  }
9979
10088
  function readClonedCheckouts(stateDir) {
9980
10089
  try {
9981
- const parsed = JSON.parse(readFileSync19(clonedCheckoutsFile(stateDir), "utf8"));
10090
+ const parsed = JSON.parse(readFileSync20(clonedCheckoutsFile(stateDir), "utf8"));
9982
10091
  return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
9983
10092
  } catch {
9984
10093
  return [];
@@ -9988,8 +10097,8 @@ function recordClonedCheckout(stateDir, dir) {
9988
10097
  const known = readClonedCheckouts(stateDir);
9989
10098
  if (known.includes(dir)) return;
9990
10099
  const file = clonedCheckoutsFile(stateDir);
9991
- mkdirSync12(dirname9(file), { recursive: true });
9992
- writeFileSync13(file, `${JSON.stringify([...known, dir], null, 2)}
10100
+ mkdirSync13(dirname9(file), { recursive: true });
10101
+ writeFileSync14(file, `${JSON.stringify([...known, dir], null, 2)}
9993
10102
  `);
9994
10103
  }
9995
10104
  var mark = (dryRun) => dryRun ? "would-remove" : "removed";
@@ -10041,8 +10150,8 @@ async function runReset(o, fx) {
10041
10150
  for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
10042
10151
  }
10043
10152
  steps.push(...removeState(o.stateDir, o.dryRun));
10044
- if (existsSync20(o.configFile)) {
10045
- if (!o.dryRun) rmSync11(o.configFile, { force: true });
10153
+ if (existsSync21(o.configFile)) {
10154
+ if (!o.dryRun) rmSync12(o.configFile, { force: true });
10046
10155
  steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
10047
10156
  } else {
10048
10157
  steps.push({ step: "config", target: o.configFile, action: "absent" });
@@ -10050,7 +10159,7 @@ async function runReset(o, fx) {
10050
10159
  return steps;
10051
10160
  }
10052
10161
  function removeCheckout(dir, dryRun, fx) {
10053
- if (!existsSync20(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
10162
+ if (!existsSync21(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
10054
10163
  const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
10055
10164
  if (dirty.length) {
10056
10165
  return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
@@ -10058,21 +10167,21 @@ function removeCheckout(dir, dryRun, fx) {
10058
10167
  if (linkedWorktrees.length) {
10059
10168
  return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
10060
10169
  }
10061
- if (!dryRun) rmSync11(dir, { recursive: true, force: true });
10170
+ if (!dryRun) rmSync12(dir, { recursive: true, force: true });
10062
10171
  return { step: "checkout", target: dir, action: mark(dryRun) };
10063
10172
  }
10064
10173
  function removeState(stateDir, dryRun) {
10065
10174
  const out = [];
10066
- const targets = [join22(stateDir, "resolved"), join22(stateDir, "keys"), clonedCheckoutsFile(stateDir), join22(stateDir, "connect-session.json"), join22(stateDir, "reload-requested"), join22(stateDir, "daemon.started")];
10175
+ const targets = [join23(stateDir, "resolved"), join23(stateDir, "keys"), clonedCheckoutsFile(stateDir), join23(stateDir, "connect-session.json"), join23(stateDir, "reload-requested"), join23(stateDir, "daemon.started")];
10067
10176
  try {
10068
- for (const name of readdirSync8(stateDir)) {
10069
- if (name.startsWith(".") && statSync6(join22(stateDir, name)).isFile()) targets.push(join22(stateDir, name));
10177
+ for (const name of readdirSync9(stateDir)) {
10178
+ if (name.startsWith(".") && statSync6(join23(stateDir, name)).isFile()) targets.push(join23(stateDir, name));
10070
10179
  }
10071
10180
  } catch {
10072
10181
  }
10073
10182
  for (const t of targets) {
10074
- if (!existsSync20(t)) continue;
10075
- if (!dryRun) rmSync11(t, { recursive: true, force: true });
10183
+ if (!existsSync21(t)) continue;
10184
+ if (!dryRun) rmSync12(t, { recursive: true, force: true });
10076
10185
  out.push({ step: "state", target: t, action: mark(dryRun) });
10077
10186
  }
10078
10187
  if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
@@ -10859,8 +10968,8 @@ ${waiting.map((t) => `| ${displayKey2(t)} | ${t.status} | ${blockers(t, i)} |`).
10859
10968
  }
10860
10969
 
10861
10970
  // src/poll.ts
10862
- import { writeFileSync as writeFileSync14 } from "fs";
10863
- import { join as join23 } from "path";
10971
+ import { writeFileSync as writeFileSync15 } from "fs";
10972
+ import { join as join24 } from "path";
10864
10973
 
10865
10974
  // src/stalled.ts
10866
10975
  var STALLED_AFTER_CYCLES = 30;
@@ -11167,7 +11276,7 @@ async function decideCycle(o) {
11167
11276
  }
11168
11277
  return decision;
11169
11278
  }
11170
- var digestPath = (stateDir, route, role, runId) => join23(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
11279
+ var digestPath = (stateDir, route, role, runId) => join24(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
11171
11280
  function otherRunsRemoved(tickets, own) {
11172
11281
  if (!own) return tickets;
11173
11282
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -11245,7 +11354,7 @@ function writeDigest(o, d, role, stateDir, runId, own) {
11245
11354
 
11246
11355
  ${body}` : body;
11247
11356
  const path = digestPath(stateDir, routeSlug(o.route.route), role, runId);
11248
- writeFileSync14(path, text);
11357
+ writeFileSync15(path, text);
11249
11358
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
11250
11359
  data: { role, bytes: Buffer.byteLength(text) }
11251
11360
  });
@@ -11728,13 +11837,13 @@ Ship ${shipName2} found: ${problem}. The release refuses to squash a ref nobody
11728
11837
  var localRefSkipReported = (ticket, problem, events) => events.some((e) => e.record_id === ticket.id && e.event_type === "local_ref_skip" && (e.message ?? "").includes(problem));
11729
11838
 
11730
11839
  // src/connect-wizard.ts
11731
- import { delimiter, dirname as dirname10, join as join24 } from "path";
11732
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync21, mkdirSync as mkdirSync13, writeFileSync as writeFileSync15 } from "fs";
11840
+ import { delimiter, dirname as dirname10, join as join25 } from "path";
11841
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync22, mkdirSync as mkdirSync14, writeFileSync as writeFileSync16 } from "fs";
11733
11842
  import { stringify } from "yaml";
11734
11843
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
11735
11844
  function shouldRunWizard(opts) {
11736
11845
  if (!opts.isTTY) return false;
11737
- const exists = opts.configExists ?? existsSync21;
11846
+ const exists = opts.configExists ?? existsSync22;
11738
11847
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
11739
11848
  }
11740
11849
  function firstRunConfigPath() {
@@ -11751,7 +11860,7 @@ function defaultIsExecutable(p) {
11751
11860
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
11752
11861
  for (const dir of pathEnv.split(delimiter)) {
11753
11862
  if (!dir) continue;
11754
- const candidate = join24(dir, name);
11863
+ const candidate = join25(dir, name);
11755
11864
  if (isExecutable(candidate)) return candidate;
11756
11865
  }
11757
11866
  return void 0;
@@ -11812,9 +11921,9 @@ routes:
11812
11921
  ${routeBlock}`;
11813
11922
  }
11814
11923
  function writeNewConfig(path, text) {
11815
- mkdirSync13(dirname10(path), { recursive: true });
11924
+ mkdirSync14(dirname10(path), { recursive: true });
11816
11925
  try {
11817
- writeFileSync15(path, text, { mode: 384, flag: "wx" });
11926
+ writeFileSync16(path, text, { mode: 384, flag: "wx" });
11818
11927
  return true;
11819
11928
  } catch (e) {
11820
11929
  if (e.code === "EEXIST") return false;
@@ -11848,7 +11957,7 @@ Wrote ${o.configPath}.
11848
11957
  // src/connect-json.ts
11849
11958
  import { accessSync as accessSync3, constants as constants3 } from "fs";
11850
11959
  import { homedir as homedir8 } from "os";
11851
- import { dirname as dirname11, join as join25 } from "path";
11960
+ import { dirname as dirname11, join as join26 } from "path";
11852
11961
  import { delimiter as delimiter2 } from "path";
11853
11962
  var CHOICE_EXIT_CODE = 3;
11854
11963
  function createEmitter(json, write) {
@@ -11887,12 +11996,12 @@ function agentBinSearchDirs(home, pathEnv, extraPath) {
11887
11996
  const dirs = [
11888
11997
  ...pathEnv.split(delimiter2),
11889
11998
  ...(extraPath ?? "").split(delimiter2),
11890
- join25(home, ".local/bin"),
11891
- join25(home, ".claude/local"),
11999
+ join26(home, ".local/bin"),
12000
+ join26(home, ".claude/local"),
11892
12001
  "/opt/homebrew/bin",
11893
12002
  "/usr/local/bin",
11894
- join25(home, ".npm-global/bin"),
11895
- join25(home, ".volta/bin")
12003
+ join26(home, ".npm-global/bin"),
12004
+ join26(home, ".volta/bin")
11896
12005
  ];
11897
12006
  return [...new Set(dirs.filter(Boolean))];
11898
12007
  }
@@ -11928,8 +12037,8 @@ function flagWizardAnswers(f, env) {
11928
12037
  }
11929
12038
 
11930
12039
  // src/release-run.ts
11931
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync16, existsSync as existsSync22, copyFileSync, mkdirSync as mkdirSync14, mkdtempSync as mkdtempSync2, rmSync as rmSync12 } from "fs";
11932
- import { dirname as dirname12, join as join26 } from "path";
12040
+ import { readFileSync as readFileSync21, writeFileSync as writeFileSync17, existsSync as existsSync23, copyFileSync, mkdirSync as mkdirSync15, mkdtempSync as mkdtempSync2, rmSync as rmSync13 } from "fs";
12041
+ import { dirname as dirname12, join as join27 } from "path";
11933
12042
  import { tmpdir as tmpdir3 } from "os";
11934
12043
 
11935
12044
  // src/release.ts
@@ -12670,13 +12779,13 @@ async function bump(o, size) {
12670
12779
  }
12671
12780
  return printed;
12672
12781
  }
12673
- const files = o.repo.release.versionFiles.map((f) => join26(o.cwd, f));
12782
+ const files = o.repo.release.versionFiles.map((f) => join27(o.cwd, f));
12674
12783
  const first = files[0];
12675
- if (!existsSync22(first)) {
12784
+ if (!existsSync23(first)) {
12676
12785
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
12677
12786
  return null;
12678
12787
  }
12679
- const current = JSON.parse(readFileSync20(first, "utf8")).version;
12788
+ const current = JSON.parse(readFileSync21(first, "utf8")).version;
12680
12789
  if (!current) {
12681
12790
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
12682
12791
  return null;
@@ -12689,27 +12798,27 @@ async function bump(o, size) {
12689
12798
  return version;
12690
12799
  }
12691
12800
  for (const f of files) {
12692
- const src = readFileSync20(f, "utf8");
12801
+ const src = readFileSync21(f, "utf8");
12693
12802
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
12694
12803
  if (out === src) {
12695
12804
  o.emit.error(`no version field in ${f}`);
12696
12805
  return null;
12697
12806
  }
12698
- writeFileSync16(f, out);
12807
+ writeFileSync17(f, out);
12699
12808
  }
12700
12809
  return version;
12701
12810
  }
12702
12811
  function writeChangelog(o, version, entries) {
12703
12812
  const file = o.repo.release.changelog;
12704
12813
  if (!file || entries.length === 0) return;
12705
- const path = join26(o.cwd, file);
12814
+ const path = join27(o.cwd, file);
12706
12815
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
12707
12816
  if (o.dryRun) {
12708
12817
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
12709
12818
  return;
12710
12819
  }
12711
- const existing = existsSync22(path) ? readFileSync20(path, "utf8") : "# Changelog\n";
12712
- writeFileSync16(path, insertChangelogSection(existing, section));
12820
+ const existing = existsSync23(path) ? readFileSync21(path, "utf8") : "# Changelog\n";
12821
+ writeFileSync17(path, insertChangelogSection(existing, section));
12713
12822
  }
12714
12823
  async function confirm(o, expected) {
12715
12824
  if (!o.repo.hooks.released) return void 0;
@@ -12792,7 +12901,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
12792
12901
  run2(cwd, ["worktree", "remove", "--force", dir]);
12793
12902
  } finally {
12794
12903
  try {
12795
- rmSync12(root, { recursive: true, force: true });
12904
+ rmSync13(root, { recursive: true, force: true });
12796
12905
  } finally {
12797
12906
  run2(cwd, ["worktree", "prune"]);
12798
12907
  run2(cwd, ["branch", "-D", branch]);
@@ -12802,8 +12911,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
12802
12911
  function openReleaseCheckout(o) {
12803
12912
  const { remote, base } = o.repo.branch;
12804
12913
  const branch = `crew/release-${Date.now()}`;
12805
- const root = mkdtempSync2(join26(tmpdir3(), "crew-release-"));
12806
- const dir = join26(root, "checkout");
12914
+ const root = mkdtempSync2(join27(tmpdir3(), "crew-release-"));
12915
+ const dir = join27(root, "checkout");
12807
12916
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
12808
12917
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
12809
12918
  try {
@@ -12813,10 +12922,10 @@ function openReleaseCheckout(o) {
12813
12922
  throw e;
12814
12923
  }
12815
12924
  for (const f of o.repo.worktrees.copy) {
12816
- const src = join26(o.cwd, f);
12817
- if (!existsSync22(src)) continue;
12818
- mkdirSync14(dirname12(join26(dir, f)), { recursive: true });
12819
- copyFileSync(src, join26(dir, f));
12925
+ const src = join27(o.cwd, f);
12926
+ if (!existsSync23(src)) continue;
12927
+ mkdirSync15(dirname12(join27(dir, f)), { recursive: true });
12928
+ copyFileSync(src, join27(dir, f));
12820
12929
  }
12821
12930
  return { dir, branch, close };
12822
12931
  }
@@ -13517,12 +13626,12 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
13517
13626
  }
13518
13627
 
13519
13628
  // src/live-runs.ts
13520
- import { existsSync as existsSync24, readdirSync as readdirSync10 } from "fs";
13521
- import { join as join28 } from "path";
13629
+ import { existsSync as existsSync25, readdirSync as readdirSync11 } from "fs";
13630
+ import { join as join29 } from "path";
13522
13631
 
13523
13632
  // src/interrupted.ts
13524
- import { existsSync as existsSync23, readdirSync as readdirSync9, readFileSync as readFileSync21 } from "fs";
13525
- import { join as join27 } from "path";
13633
+ import { existsSync as existsSync24, readdirSync as readdirSync10, readFileSync as readFileSync22 } from "fs";
13634
+ import { join as join28 } from "path";
13526
13635
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
13527
13636
  function interruptedComment(b) {
13528
13637
  const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
@@ -13541,21 +13650,21 @@ var pidAlive = (pid) => {
13541
13650
  };
13542
13651
  function runFinished(streamPath) {
13543
13652
  const path = `${streamPath}.meta.json`;
13544
- if (!existsSync23(path)) return false;
13653
+ if (!existsSync24(path)) return false;
13545
13654
  try {
13546
- return JSON.parse(readFileSync21(path, "utf8")).running !== true;
13655
+ return JSON.parse(readFileSync22(path, "utf8")).running !== true;
13547
13656
  } catch {
13548
13657
  return true;
13549
13658
  }
13550
13659
  }
13551
13660
  function orphanedStreams(o) {
13552
- const dir = join27(o.stateDir, "streams");
13553
- if (!existsSync23(dir)) return [];
13661
+ const dir = join28(o.stateDir, "streams");
13662
+ if (!existsSync24(dir)) return [];
13554
13663
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
13555
13664
  const out = [];
13556
- for (const name of readdirSync9(dir)) {
13665
+ for (const name of readdirSync10(dir)) {
13557
13666
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13558
- const streamPath = join27(dir, name);
13667
+ const streamPath = join28(dir, name);
13559
13668
  if (runFinished(streamPath)) continue;
13560
13669
  const sidecar = readRunSidecar(streamPath);
13561
13670
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -13609,12 +13718,12 @@ async function reconcileRuns(o) {
13609
13718
 
13610
13719
  // src/live-runs.ts
13611
13720
  function liveRuns(stateDir, isAlive = runIsAlive) {
13612
- const dir = join28(stateDir, "streams");
13613
- if (!existsSync24(dir)) return [];
13721
+ const dir = join29(stateDir, "streams");
13722
+ if (!existsSync25(dir)) return [];
13614
13723
  const out = [];
13615
- for (const name of readdirSync10(dir)) {
13724
+ for (const name of readdirSync11(dir)) {
13616
13725
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13617
- const streamPath = join28(dir, name);
13726
+ const streamPath = join29(dir, name);
13618
13727
  if (runFinished(streamPath)) continue;
13619
13728
  const sidecar = readRunSidecar(streamPath);
13620
13729
  if (!sidecar || !isAlive(sidecar)) continue;
@@ -13884,7 +13993,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
13884
13993
  }
13885
13994
 
13886
13995
  // src/watch.ts
13887
- import { watch as watchFile, existsSync as existsSync25, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
13996
+ import { watch as watchFile, existsSync as existsSync26, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
13888
13997
  var E = "\x1B[";
13889
13998
  var dim = (s) => `${E}2m${s}${E}0m`;
13890
13999
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -13985,7 +14094,7 @@ function renderHeaders(states, now = Date.now()) {
13985
14094
  return states.map((s) => renderHeader(s, now)).join("");
13986
14095
  }
13987
14096
  function readFrom(file, from) {
13988
- if (!existsSync25(file)) return { events: [], offset: from };
14097
+ if (!existsSync26(file)) return { events: [], offset: from };
13989
14098
  const size = statSync7(file).size;
13990
14099
  if (size < from) return readFrom(file, 0);
13991
14100
  if (size === from) return { events: [], offset: from };
@@ -14228,17 +14337,17 @@ function nextRoles(decision, ran) {
14228
14337
  }
14229
14338
 
14230
14339
  // src/ship-name.ts
14231
- import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync23, renameSync as renameSync2, writeFileSync as writeFileSync18 } from "fs";
14340
+ import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, renameSync as renameSync2, writeFileSync as writeFileSync19 } from "fs";
14232
14341
  import { dirname as dirname14 } from "path";
14233
14342
  import { parseDocument } from "yaml";
14234
14343
 
14235
14344
  // src/machine-id.ts
14236
14345
  import { execFileSync as execFileSync4 } from "child_process";
14237
14346
  import { createHash as createHash4 } from "crypto";
14238
- import { existsSync as existsSync26, mkdirSync as mkdirSync15, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
14239
- import { dirname as dirname13, join as join29 } from "path";
14347
+ import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync23, writeFileSync as writeFileSync18 } from "fs";
14348
+ import { dirname as dirname13, join as join30 } from "path";
14240
14349
  function machineIdPathFor(stateDir) {
14241
- return join29(stateDir, "machine-id");
14350
+ return join30(stateDir, "machine-id");
14242
14351
  }
14243
14352
  var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
14244
14353
  function readRawMachineId(platform2 = process.platform) {
@@ -14251,8 +14360,8 @@ function readRawMachineId(platform2 = process.platform) {
14251
14360
  return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
14252
14361
  }
14253
14362
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
14254
- if (existsSync26(p)) {
14255
- const id = readFileSync22(p, "utf8").trim();
14363
+ if (existsSync27(p)) {
14364
+ const id = readFileSync23(p, "utf8").trim();
14256
14365
  if (id) return id;
14257
14366
  }
14258
14367
  }
@@ -14263,15 +14372,15 @@ function readRawMachineId(platform2 = process.platform) {
14263
14372
  function ensureMachineId(stateDir, opts = {}) {
14264
14373
  if (opts.override?.trim()) return sha256(opts.override.trim());
14265
14374
  const path = machineIdPathFor(stateDir);
14266
- if (existsSync26(path)) {
14267
- const cached = readFileSync22(path, "utf8").trim();
14375
+ if (existsSync27(path)) {
14376
+ const cached = readFileSync23(path, "utf8").trim();
14268
14377
  if (/^[0-9a-f]{64}$/.test(cached)) return cached;
14269
14378
  }
14270
14379
  const raw = (opts.read ?? readRawMachineId)();
14271
14380
  if (!raw) return void 0;
14272
14381
  const hashed = sha256(raw);
14273
- mkdirSync15(dirname13(path), { recursive: true });
14274
- writeFileSync17(path, `${hashed}
14382
+ mkdirSync16(dirname13(path), { recursive: true });
14383
+ writeFileSync18(path, `${hashed}
14275
14384
  `, { mode: 384 });
14276
14385
  return hashed;
14277
14386
  }
@@ -14342,34 +14451,34 @@ async function syncRouteName(i) {
14342
14451
  return { status: "ok", ...base, action, name: i.local };
14343
14452
  }
14344
14453
  function readSyncedName(resolvedFile) {
14345
- if (!existsSync27(resolvedFile)) return void 0;
14454
+ if (!existsSync28(resolvedFile)) return void 0;
14346
14455
  try {
14347
- const v = JSON.parse(readFileSync23(resolvedFile, "utf8")).shipName;
14456
+ const v = JSON.parse(readFileSync24(resolvedFile, "utf8")).shipName;
14348
14457
  return typeof v === "string" ? v : void 0;
14349
14458
  } catch {
14350
14459
  return void 0;
14351
14460
  }
14352
14461
  }
14353
14462
  function writeSyncedName(resolvedFile, name) {
14354
- if (!existsSync27(resolvedFile)) return;
14463
+ if (!existsSync28(resolvedFile)) return;
14355
14464
  let raw;
14356
14465
  try {
14357
- raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
14466
+ raw = JSON.parse(readFileSync24(resolvedFile, "utf8"));
14358
14467
  } catch {
14359
14468
  return;
14360
14469
  }
14361
14470
  if (raw.shipName === name) return;
14362
14471
  raw.shipName = name;
14363
- mkdirSync16(dirname14(resolvedFile), { recursive: true });
14472
+ mkdirSync17(dirname14(resolvedFile), { recursive: true });
14364
14473
  const tmp = `${resolvedFile}.tmp${process.pid}`;
14365
- writeFileSync18(tmp, `${JSON.stringify(raw, null, 2)}
14474
+ writeFileSync19(tmp, `${JSON.stringify(raw, null, 2)}
14366
14475
  `);
14367
14476
  renameSync2(tmp, resolvedFile);
14368
14477
  }
14369
14478
  async function reconcileShipName(i) {
14370
14479
  let local = i.ship.name;
14371
14480
  try {
14372
- local = shipNameOfYaml(readFileSync23(i.configFile, "utf8")) ?? local;
14481
+ local = shipNameOfYaml(readFileSync24(i.configFile, "utf8")) ?? local;
14373
14482
  } catch {
14374
14483
  }
14375
14484
  const resolvedFile = resolvedPathFor(i.stateDir, i.route);
@@ -14393,8 +14502,8 @@ async function reconcileShipName(i) {
14393
14502
  }
14394
14503
  if (result.status !== "ok") return result;
14395
14504
  if (result.action === "adopt" || result.action === "row-wins") {
14396
- const text = readFileSync23(i.configFile, "utf8");
14397
- writeFileSync18(i.configFile, setShipNameInYaml(text, result.name));
14505
+ const text = readFileSync24(i.configFile, "utf8");
14506
+ writeFileSync19(i.configFile, setShipNameInYaml(text, result.name));
14398
14507
  i.ship.name = result.name;
14399
14508
  if (result.action === "row-wins") {
14400
14509
  i.emit?.warn(
@@ -15044,7 +15153,7 @@ function describeCapability(i) {
15044
15153
  }
15045
15154
 
15046
15155
  // src/agents-sync-run.ts
15047
- import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, writeFileSync as writeFileSync19 } from "fs";
15156
+ import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, writeFileSync as writeFileSync20 } from "fs";
15048
15157
  import { dirname as dirname15 } from "path";
15049
15158
  async function runAgentsSync(target, o) {
15050
15159
  const dryRun = o.dryRun ?? false;
@@ -15078,12 +15187,12 @@ async function runAgentsSync(target, o) {
15078
15187
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
15079
15188
  if (!dryRun) {
15080
15189
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
15081
- const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync24(resolvedPath, "utf8")) : {};
15190
+ const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync25(resolvedPath, "utf8")) : {};
15082
15191
  raw.agentPersonas = personaResult.agentPersonas;
15083
15192
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
15084
15193
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
15085
- mkdirSync17(dirname15(resolvedPath), { recursive: true });
15086
- writeFileSync19(resolvedPath, `${JSON.stringify(raw, null, 2)}
15194
+ mkdirSync18(dirname15(resolvedPath), { recursive: true });
15195
+ writeFileSync20(resolvedPath, `${JSON.stringify(raw, null, 2)}
15087
15196
  `);
15088
15197
  if (target.resolved) {
15089
15198
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -15187,7 +15296,7 @@ async function showLogEntry(route, entryId, opts = {}) {
15187
15296
 
15188
15297
  // src/repos-check.ts
15189
15298
  import { spawnSync as spawnSync6 } from "child_process";
15190
- import { readFileSync as readFileSync25 } from "fs";
15299
+ import { readFileSync as readFileSync26 } from "fs";
15191
15300
  import { basename as basename8 } from "path";
15192
15301
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
15193
15302
  function checkHookSyntax(script, shell) {
@@ -15228,7 +15337,7 @@ function checkRepoSpec(dir, opts = {}) {
15228
15337
  }
15229
15338
  let parsed;
15230
15339
  try {
15231
- parsed = parseRepoConfig(readFileSync25(file, "utf8"), file);
15340
+ parsed = parseRepoConfig(readFileSync26(file, "utf8"), file);
15232
15341
  } catch (e) {
15233
15342
  if (!(e instanceof RepoConfigError)) throw e;
15234
15343
  return { ...empty, file, error: e.message, exitCode: 1 };
@@ -15285,7 +15394,7 @@ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml"
15285
15394
 
15286
15395
  // src/repos-cmd.ts
15287
15396
  import { homedir as homedir9 } from "os";
15288
- import { basename as basename9, join as join30, resolve as resolve3 } from "path";
15397
+ import { basename as basename9, join as join31, resolve as resolve3 } from "path";
15289
15398
  import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
15290
15399
  var ReposError = class extends Error {
15291
15400
  };
@@ -15563,7 +15672,7 @@ function planRepoClone(i) {
15563
15672
  }
15564
15673
  const ship = doc.get("ship", true);
15565
15674
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
15566
- const target = i.dir ? expandDir(i.dir, i.base) : join30(expandDir(basePath, i.base), i.route.split("/")[0], name);
15675
+ const target = i.dir ? expandDir(i.dir, i.base) : join31(expandDir(basePath, i.base), i.route.split("/")[0], name);
15567
15676
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
15568
15677
  return { ...add, remote, target };
15569
15678
  }
@@ -15962,7 +16071,7 @@ ${run2.err}` });
15962
16071
  }
15963
16072
 
15964
16073
  // src/sample-ticket.ts
15965
- import { basename as basename10, dirname as dirname17, join as join31 } from "path";
16074
+ import { basename as basename10, dirname as dirname17, join as join32 } from "path";
15966
16075
  var SAMPLE_REPO = "crew-sample";
15967
16076
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
15968
16077
  var SAMPLE_MARKER = "sample: true";
@@ -15985,8 +16094,8 @@ has something safe to work on: its test and build hooks always pass, it has no
15985
16094
  remote, and \`crew sample-ticket --remove\` deletes it.
15986
16095
  `;
15987
16096
  function sampleDir(existing, reposBase, workspace) {
15988
- if (existing.length > 0) return join31(dirname17(existing[0]), SAMPLE_REPO);
15989
- return join31(reposBase, workspace, SAMPLE_REPO);
16097
+ if (existing.length > 0) return join32(dirname17(existing[0]), SAMPLE_REPO);
16098
+ return join32(reposBase, workspace, SAMPLE_REPO);
15990
16099
  }
15991
16100
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
15992
16101
  function sampleDescription() {
@@ -16078,8 +16187,8 @@ function schedulerInstalled(plan, exists, crontab) {
16078
16187
  }
16079
16188
 
16080
16189
  // src/worktree-sweep.ts
16081
- import { existsSync as existsSync29, readFileSync as readFileSync26, readdirSync as readdirSync11 } from "fs";
16082
- import { basename as basename11, dirname as dirname18, join as join32, resolve as resolve4 } from "path";
16190
+ import { existsSync as existsSync30, readFileSync as readFileSync27, readdirSync as readdirSync12 } from "fs";
16191
+ import { basename as basename11, dirname as dirname18, join as join33, resolve as resolve4 } from "path";
16083
16192
  function planWorktreeSweep(target, tickets, contract) {
16084
16193
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
16085
16194
  const verified = contract.statuses.verified;
@@ -16138,7 +16247,7 @@ function siblingWorktreeDirs(target) {
16138
16247
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
16139
16248
  let names;
16140
16249
  try {
16141
- names = readdirSync11(parent);
16250
+ names = readdirSync12(parent);
16142
16251
  } catch {
16143
16252
  return [];
16144
16253
  }
@@ -16146,7 +16255,7 @@ function siblingWorktreeDirs(target) {
16146
16255
  for (const name of names) {
16147
16256
  if (!name.startsWith(stem)) continue;
16148
16257
  const m = DIR_NUMBER_SUFFIXED.exec(name);
16149
- const path = join32(parent, name);
16258
+ const path = join33(parent, name);
16150
16259
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
16151
16260
  }
16152
16261
  return out;
@@ -16181,7 +16290,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16181
16290
  let removed = 0;
16182
16291
  let keptBranches = 0;
16183
16292
  for (const a of actions) {
16184
- if (!dryRun && base && !a.keepBranch && existsSync29(a.path) && isHandBackMerge(a.path, base)) {
16293
+ if (!dryRun && base && !a.keepBranch && existsSync30(a.path) && isHandBackMerge(a.path, base)) {
16185
16294
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
16186
16295
  log.emit(
16187
16296
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -16189,7 +16298,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16189
16298
  );
16190
16299
  }
16191
16300
  }
16192
- if (!dryRun && existsSync29(a.path)) {
16301
+ if (!dryRun && existsSync30(a.path)) {
16193
16302
  let dirty = false;
16194
16303
  try {
16195
16304
  dirty = status(a.path).length > 0;
@@ -16200,7 +16309,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
16200
16309
  continue;
16201
16310
  }
16202
16311
  }
16203
- if (!dryRun && a.requirePushed && a.branch && existsSync29(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
16312
+ if (!dryRun && a.requirePushed && a.branch && existsSync30(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
16204
16313
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
16205
16314
  continue;
16206
16315
  }
@@ -16249,7 +16358,7 @@ function isHandBackMerge(path, base) {
16249
16358
  if (!msgPath) return false;
16250
16359
  let first;
16251
16360
  try {
16252
- first = readFileSync26(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16361
+ first = readFileSync27(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16253
16362
  } catch {
16254
16363
  return false;
16255
16364
  }
@@ -16354,14 +16463,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
16354
16463
  }
16355
16464
 
16356
16465
  // src/stream-sweep.ts
16357
- import { existsSync as existsSync30, readdirSync as readdirSync12, rmSync as rmSync13, statSync as statSync8 } from "fs";
16358
- import { join as join33 } from "path";
16466
+ import { existsSync as existsSync31, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync8 } from "fs";
16467
+ import { join as join34 } from "path";
16359
16468
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
16360
16469
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
16361
- const dir = join33(stateDir, "streams");
16362
- if (!existsSync30(dir)) return [];
16470
+ const dir = join34(stateDir, "streams");
16471
+ if (!existsSync31(dir)) return [];
16363
16472
  const cutoff = now - retentionDays * DAY_MS2;
16364
- return readdirSync12(dir).map((name) => join33(dir, name)).filter((path) => {
16473
+ return readdirSync13(dir).map((name) => join34(dir, name)).filter((path) => {
16365
16474
  try {
16366
16475
  return statSync8(path).mtimeMs < cutoff;
16367
16476
  } catch {
@@ -16377,7 +16486,7 @@ function applyStreamSweep(paths, dryRun, log) {
16377
16486
  continue;
16378
16487
  }
16379
16488
  try {
16380
- rmSync13(path, { force: true });
16489
+ rmSync14(path, { force: true });
16381
16490
  removed++;
16382
16491
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
16383
16492
  } catch (e) {
@@ -16387,9 +16496,9 @@ function applyStreamSweep(paths, dryRun, log) {
16387
16496
  return removed;
16388
16497
  }
16389
16498
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16390
- if (!existsSync30(stateDir)) return [];
16499
+ if (!existsSync31(stateDir)) return [];
16391
16500
  const cutoff = now - retentionDays * DAY_MS2;
16392
- return readdirSync12(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join33(stateDir, name)).filter((path) => {
16501
+ return readdirSync13(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join34(stateDir, name)).filter((path) => {
16393
16502
  try {
16394
16503
  return statSync8(path).mtimeMs < cutoff;
16395
16504
  } catch {
@@ -16399,7 +16508,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16399
16508
  }
16400
16509
 
16401
16510
  // src/cli.ts
16402
- import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as existsSync35, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync16 } from "fs";
16511
+ import { realpathSync as realpathSync3, readFileSync as readFileSync32, existsSync as existsSync36, mkdirSync as mkdirSync23, writeFileSync as writeFileSync24, rmSync as rmSync16, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync17 } from "fs";
16403
16512
  import { createInterface } from "readline/promises";
16404
16513
 
16405
16514
  // src/periscope-cmd.ts
@@ -16527,29 +16636,160 @@ ${tail(preview.output)}`);
16527
16636
  return abandon(`recording the Periscopes row failed: ${e.message}`);
16528
16637
  }
16529
16638
  d.say("recorded", "recorded the Periscopes row", { periscopeRecordId: row.id });
16639
+ try {
16640
+ d.recordLocal?.({
16641
+ ticket: draft.tag,
16642
+ ticketRecordId: draft.id,
16643
+ periscopeRecordId: row.id,
16644
+ repo: args.repo,
16645
+ worktree: wt.path,
16646
+ branch: wt.branch,
16647
+ url,
16648
+ host: d.host,
16649
+ state: "live",
16650
+ isolated: args.isolate,
16651
+ startedAt: now.toISOString(),
16652
+ lastActivityAt: now.toISOString(),
16653
+ expiresAt: new Date(now.getTime() + d.caps.idleMinutes * 6e4).toISOString()
16654
+ });
16655
+ } catch (e) {
16656
+ d.say("warning", `local periscope record not written: ${e.message}`);
16657
+ }
16530
16658
  await d.comment(draft.id, progressComment(url, handoff?.output)).catch((e) => {
16531
16659
  d.say("warning", `progress comment not posted: ${e.message}`);
16532
16660
  });
16533
16661
  return { ticket: draft.tag, ticketRecordId: draft.id, worktree: wt.path, branch: wt.branch, url, periscopeRecordId: row.id };
16534
16662
  }
16663
+ function parsePeriscopeDownArgs(positional2, flags) {
16664
+ const usage2 = "usage: crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]";
16665
+ const rest = positional2.slice(2);
16666
+ if (rest.length === 0 || rest.length > 2) throw new PeriscopeError(usage2);
16667
+ if (flags.keep === flags.discard) throw new PeriscopeError(`pass exactly one of --keep or --discard
16668
+ ${usage2}`);
16669
+ const m = /^(?:[A-Za-z]+-)?(\d+)$/.exec(rest[rest.length - 1]);
16670
+ if (!m) throw new PeriscopeError(`"${rest[rest.length - 1]}" is not a ticket number
16671
+ ${usage2}`);
16672
+ return {
16673
+ ...rest.length === 2 ? { route: rest[0] } : {},
16674
+ number: m[1],
16675
+ mode: flags.keep ? "keep" : "discard",
16676
+ allowEmpty: flags.allowEmpty,
16677
+ json: flags.json
16678
+ };
16679
+ }
16680
+ var PROTOTYPE_HEADING = "## Prototype";
16681
+ function prototypeSection(files) {
16682
+ const added = files.reduce((n, f) => n + f.added, 0);
16683
+ const removed = files.reduce((n, f) => n + f.removed, 0);
16684
+ return [
16685
+ PROTOTYPE_HEADING,
16686
+ "",
16687
+ "This branch began as a Periscope prototype and may be rewritten rather than treated as reviewed work.",
16688
+ "",
16689
+ ...files.length === 0 ? ["No files differ from the base."] : files.map((f) => `- \`${f.path}\` (+${f.added} \u2212${f.removed})`),
16690
+ "",
16691
+ `${files.length} file${files.length === 1 ? "" : "s"} changed, ${added} insertion${added === 1 ? "" : "s"}(+), ${removed} deletion${removed === 1 ? "" : "s"}(\u2212)`
16692
+ ].join("\n");
16693
+ }
16694
+ function forgetLocal(d, ticketRecordId) {
16695
+ try {
16696
+ d.forgetLocal?.(ticketRecordId);
16697
+ } catch (e) {
16698
+ d.say("warn", `could not remove the local periscope record: ${e.message}`);
16699
+ }
16700
+ }
16701
+ async function runPeriscopeDown(d, args) {
16702
+ const rows = await d.rowsForTicket(args.number);
16703
+ if (rows.length === 0) throw new PeriscopeError(`no periscope for ticket ${args.number} on this ship`);
16704
+ const row = rows.find((r) => r.state === "live");
16705
+ if (!row) {
16706
+ const done = rows[0];
16707
+ d.say("unchanged", `${done.ticket} is already ${done.state}; nothing to do`);
16708
+ return { ticket: done.ticket, mode: args.mode, outcome: "unchanged", state: done.state };
16709
+ }
16710
+ if (args.mode === "discard") {
16711
+ const killed2 = await d.stopServer(row.worktree, row.number);
16712
+ d.say("server", killed2 ? `stopped ${killed2} process(es)` : "no server was listening");
16713
+ d.removeWorktree(row.worktree, row.branch, true);
16714
+ d.say("worktree", `removed ${row.worktree} and branch ${row.branch}`);
16715
+ await d.updateTicket(row.ticketRecordId, {
16716
+ status: "discarded",
16717
+ fillProjectAndRepo: false,
16718
+ note: "The periscope was discarded; nothing was pushed."
16719
+ });
16720
+ d.say("ticket", `${row.ticket} closed as obsolete`);
16721
+ await d.setRowState(row.id, "discarded");
16722
+ d.say("row", "row is discarded");
16723
+ forgetLocal(d, row.ticketRecordId);
16724
+ return { ticket: row.ticket, mode: "discard", outcome: "done", state: "discarded" };
16725
+ }
16726
+ const dirty = d.hasChanges(row.worktree);
16727
+ if (!dirty && !args.allowEmpty) {
16728
+ throw new PeriscopeError(`${row.ticket} has nothing to commit; pass --allow-empty to keep the branch anyway, or --discard`);
16729
+ }
16730
+ if (dirty) {
16731
+ d.commitAll(row.worktree, `${row.ticket} Periscope prototype`);
16732
+ d.say("commit", `committed the worktree on ${row.branch}`);
16733
+ }
16734
+ const pushed = d.push(row.worktree, row.branch);
16735
+ d.say("push", `pushed ${pushed.sha} to ${pushed.remote}/${row.branch}`, { sha: pushed.sha });
16736
+ const t = await d.readTicket(row.ticketRecordId);
16737
+ const section = prototypeSection(d.changedFiles(row.worktree));
16738
+ const description = t.description.includes(PROTOTYPE_HEADING) ? t.description : `${t.description.trimEnd()}
16739
+
16740
+ ${section}`;
16741
+ await d.updateTicket(row.ticketRecordId, {
16742
+ description,
16743
+ fillProjectAndRepo: !t.hasProject || !t.hasRepo,
16744
+ status: "new"
16745
+ });
16746
+ d.say("ticket", `${row.ticket} is new and unassigned with a Prototype section`);
16747
+ await d.postPushed(row.ticketRecordId, pushed.sha, pushed.remote, row.branch);
16748
+ const killed = await d.stopServer(row.worktree, row.number);
16749
+ d.say("server", killed ? `stopped ${killed} process(es)` : "no server was listening");
16750
+ d.removeWorktree(row.worktree, row.branch, false);
16751
+ d.say("worktree", `removed ${row.worktree}; ${row.branch} stays, pushed`);
16752
+ await d.setRowState(row.id, "kept");
16753
+ d.say("row", "row is kept");
16754
+ forgetLocal(d, row.ticketRecordId);
16755
+ return { ticket: row.ticket, mode: "keep", outcome: "done", state: "kept", sha: pushed.sha };
16756
+ }
16757
+ function periscopeListEntries(rows, now, isListening) {
16758
+ return rows.filter((r) => r.state === "live").map((r) => {
16759
+ const at = r.lastActivityAt ? new Date(r.lastActivityAt).getTime() : NaN;
16760
+ return { ...r, idleMinutes: Number.isNaN(at) ? null : Math.max(0, Math.floor((now.getTime() - at) / 6e4)), listening: isListening(r) };
16761
+ });
16762
+ }
16763
+ function formatPeriscopeList(entries) {
16764
+ if (entries.length === 0) return "no live periscopes on this ship";
16765
+ return entries.map((e) => [
16766
+ e.ticket,
16767
+ e.repo,
16768
+ e.worktree,
16769
+ e.url,
16770
+ e.state,
16771
+ e.idleMinutes === null ? "idle ?" : `idle ${e.idleMinutes} min`,
16772
+ e.listening ? "listening" : "not listening"
16773
+ ].join(" ")).join("\n");
16774
+ }
16535
16775
 
16536
16776
  // src/cli.ts
16537
- import { dirname as dirOf, resolve as resolvePath } from "path";
16777
+ import { dirname as dirOf2, resolve as resolvePath } from "path";
16538
16778
 
16539
16779
  // src/ssh-keys.ts
16540
16780
  import { execFileSync as execFileSync6 } from "child_process";
16541
- import { existsSync as existsSync31, mkdirSync as mkdirSync18, readFileSync as readFileSync27, chmodSync } from "fs";
16542
- import { dirname as dirname19, join as join34 } from "path";
16781
+ import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, chmodSync } from "fs";
16782
+ import { dirname as dirname19, join as join35 } from "path";
16543
16783
  var SshKeyError = class extends Error {
16544
16784
  };
16545
16785
  function shipSshKeyPathFor(stateDir) {
16546
- return join34(stateDir, "ssh", "id_ed25519");
16786
+ return join35(stateDir, "ssh", "id_ed25519");
16547
16787
  }
16548
16788
  function ensureShipSshKeypair(stateDir) {
16549
16789
  const privateKeyPath = shipSshKeyPathFor(stateDir);
16550
16790
  const publicKeyPath = `${privateKeyPath}.pub`;
16551
- if (!existsSync31(privateKeyPath)) {
16552
- mkdirSync18(dirname19(privateKeyPath), { recursive: true });
16791
+ if (!existsSync32(privateKeyPath)) {
16792
+ mkdirSync19(dirname19(privateKeyPath), { recursive: true });
16553
16793
  try {
16554
16794
  execFileSync6("ssh-keygen", [
16555
16795
  "-t",
@@ -16569,10 +16809,10 @@ function ensureShipSshKeypair(stateDir) {
16569
16809
  }
16570
16810
  chmodSync(privateKeyPath, 384);
16571
16811
  }
16572
- if (!existsSync31(publicKeyPath)) {
16812
+ if (!existsSync32(publicKeyPath)) {
16573
16813
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
16574
16814
  }
16575
- const publicKey = readFileSync27(publicKeyPath, "utf8").trim();
16815
+ const publicKey = readFileSync28(publicKeyPath, "utf8").trim();
16576
16816
  return { privateKeyPath, publicKeyPath, publicKey };
16577
16817
  }
16578
16818
  function sshKeygenAvailable() {
@@ -16789,9 +17029,9 @@ function syncAllPassengerCheckouts(cfg) {
16789
17029
 
16790
17030
  // src/tunnel.ts
16791
17031
  import { spawn as spawn5 } from "child_process";
16792
- import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
17032
+ import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync21, unlinkSync as unlinkSync3, readdirSync as readdirSync14 } from "fs";
16793
17033
  import { randomInt } from "crypto";
16794
- import { join as join35 } from "path";
17034
+ import { join as join36 } from "path";
16795
17035
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
16796
17036
  var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
16797
17037
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -16858,21 +17098,21 @@ function startTunnel(opts) {
16858
17098
  };
16859
17099
  }
16860
17100
  function tunnelStatePath(stateDir, workspaceId) {
16861
- return join35(stateDir, "passengers", `${workspaceId}.json`);
17101
+ return join36(stateDir, "passengers", `${workspaceId}.json`);
16862
17102
  }
16863
17103
  function readPersistedTunnel(stateDir, workspaceId) {
16864
17104
  const path = tunnelStatePath(stateDir, workspaceId);
16865
- if (!existsSync32(path)) return void 0;
17105
+ if (!existsSync33(path)) return void 0;
16866
17106
  try {
16867
- return JSON.parse(readFileSync28(path, "utf8"));
17107
+ return JSON.parse(readFileSync29(path, "utf8"));
16868
17108
  } catch {
16869
17109
  return void 0;
16870
17110
  }
16871
17111
  }
16872
17112
  function writePersistedTunnel(stateDir, state) {
16873
17113
  const path = tunnelStatePath(stateDir, state.workspaceId);
16874
- mkdirSync19(join35(stateDir, "passengers"), { recursive: true });
16875
- writeFileSync20(path, `${JSON.stringify(state, null, 2)}
17114
+ mkdirSync20(join36(stateDir, "passengers"), { recursive: true });
17115
+ writeFileSync21(path, `${JSON.stringify(state, null, 2)}
16876
17116
  `);
16877
17117
  }
16878
17118
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -16969,9 +17209,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
16969
17209
  }
16970
17210
  }
16971
17211
  function readAllPersistedWorkspaceIds(stateDir) {
16972
- const dir = join35(stateDir, "passengers");
16973
- if (!existsSync32(dir)) return [];
16974
- return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
17212
+ const dir = join36(stateDir, "passengers");
17213
+ if (!existsSync33(dir)) return [];
17214
+ return readdirSync14(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
16975
17215
  }
16976
17216
 
16977
17217
  // src/mcp-endpoint-registry.ts
@@ -17091,8 +17331,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
17091
17331
  import { createServer as createHttpServer } from "http";
17092
17332
  import { spawn as spawn6 } from "child_process";
17093
17333
  import { randomBytes } from "crypto";
17094
- import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync14 } from "fs";
17095
- import { join as join36 } from "path";
17334
+ import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, writeFileSync as writeFileSync22, unlinkSync as unlinkSync4, readdirSync as readdirSync15 } from "fs";
17335
+ import { join as join37 } from "path";
17096
17336
 
17097
17337
  // src/secret-compare.ts
17098
17338
  import { timingSafeEqual } from "crypto";
@@ -17181,21 +17421,21 @@ var defaultSpawn2 = (cmd, args, env) => {
17181
17421
  return child;
17182
17422
  };
17183
17423
  function syncDaemonStatePath(stateDir, workspaceId) {
17184
- return join36(stateDir, "passengers", `${workspaceId}-sync.json`);
17424
+ return join37(stateDir, "passengers", `${workspaceId}-sync.json`);
17185
17425
  }
17186
17426
  function readPersistedSyncDaemon(stateDir, workspaceId) {
17187
17427
  const path = syncDaemonStatePath(stateDir, workspaceId);
17188
- if (!existsSync33(path)) return void 0;
17428
+ if (!existsSync34(path)) return void 0;
17189
17429
  try {
17190
- return JSON.parse(readFileSync29(path, "utf8"));
17430
+ return JSON.parse(readFileSync30(path, "utf8"));
17191
17431
  } catch {
17192
17432
  return void 0;
17193
17433
  }
17194
17434
  }
17195
17435
  function writePersistedSyncDaemon(stateDir, state) {
17196
17436
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
17197
- mkdirSync20(join36(stateDir, "passengers"), { recursive: true });
17198
- writeFileSync21(path, `${JSON.stringify(state, null, 2)}
17437
+ mkdirSync21(join37(stateDir, "passengers"), { recursive: true });
17438
+ writeFileSync22(path, `${JSON.stringify(state, null, 2)}
17199
17439
  `);
17200
17440
  }
17201
17441
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -17215,10 +17455,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
17215
17455
  }
17216
17456
  }
17217
17457
  function readAllPersistedSyncWorkspaceIds(stateDir) {
17218
- const dir = join36(stateDir, "passengers");
17219
- if (!existsSync33(dir)) return [];
17458
+ const dir = join37(stateDir, "passengers");
17459
+ if (!existsSync34(dir)) return [];
17220
17460
  const suffix = "-sync.json";
17221
- return readdirSync14(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
17461
+ return readdirSync15(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
17222
17462
  }
17223
17463
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
17224
17464
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -17304,8 +17544,8 @@ function describeCause(err) {
17304
17544
  }
17305
17545
 
17306
17546
  // src/daemon.ts
17307
- import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync15, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
17308
- import { join as join37 } from "path";
17547
+ import { existsSync as existsSync35, mkdirSync as mkdirSync22, readFileSync as readFileSync31, readdirSync as readdirSync16, rmSync as rmSync15, statSync as statSync9, writeFileSync as writeFileSync23 } from "fs";
17548
+ import { join as join38 } from "path";
17309
17549
  import { randomBytes as randomBytes2 } from "crypto";
17310
17550
  var DEFAULT_FLOOR_MS = 5e3;
17311
17551
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
@@ -17375,8 +17615,8 @@ function latestMtime(path) {
17375
17615
  if (stat.isFile()) return stat.mtimeMs;
17376
17616
  if (!stat.isDirectory()) return 0;
17377
17617
  let latest = stat.mtimeMs;
17378
- for (const entry of readdirSync15(path, { withFileTypes: true })) {
17379
- latest = Math.max(latest, latestMtime(join37(path, entry.name)));
17618
+ for (const entry of readdirSync16(path, { withFileTypes: true })) {
17619
+ latest = Math.max(latest, latestMtime(join38(path, entry.name)));
17380
17620
  }
17381
17621
  return latest;
17382
17622
  }
@@ -17385,23 +17625,23 @@ function computeTreeSignature(paths) {
17385
17625
  }
17386
17626
  function makeStaleChecker(paths, opts = {}) {
17387
17627
  const initial = computeTreeSignature(paths);
17388
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync34(opts.reloadSentinel);
17628
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync35(opts.reloadSentinel);
17389
17629
  }
17390
17630
  function reloadSentinelFor(stateDir) {
17391
- return join37(stateDir, "reload-requested");
17631
+ return join38(stateDir, "reload-requested");
17392
17632
  }
17393
17633
  function daemonStampFor(stateDir) {
17394
- return join37(stateDir, "daemon.started");
17634
+ return join38(stateDir, "daemon.started");
17395
17635
  }
17396
17636
  function noteDaemonStarted(stateDir, now = Date.now()) {
17397
- mkdirSync21(stateDir, { recursive: true });
17398
- rmSync14(reloadSentinelFor(stateDir), { force: true });
17399
- writeFileSync22(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
17637
+ mkdirSync22(stateDir, { recursive: true });
17638
+ rmSync15(reloadSentinelFor(stateDir), { force: true });
17639
+ writeFileSync23(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
17400
17640
  `);
17401
17641
  }
17402
17642
  function readDaemonStarted(stateDir) {
17403
17643
  try {
17404
- const v = JSON.parse(readFileSync30(daemonStampFor(stateDir), "utf8")).startedAt;
17644
+ const v = JSON.parse(readFileSync31(daemonStampFor(stateDir), "utf8")).startedAt;
17405
17645
  return typeof v === "number" ? v : null;
17406
17646
  } catch {
17407
17647
  return null;
@@ -17996,6 +18236,9 @@ function usage(problem, requested = false) {
17996
18236
  crew rotate-passenger-url [route] force a Host Passengers tunnel to reconnect with a fresh public URL
17997
18237
  crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]
17998
18238
  file a draft ticket, cut and prepare its worktree, start the repo's preview hook and record a Periscopes row (alias: start)
18239
+ crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]
18240
+ end a periscope: commit and push its branch (ticket back to new), or throw it away (alias: end)
18241
+ crew periscope list [route] [--json] the live periscopes on this ship, their idle minutes and whether each server is listening
17999
18242
  crew worktree [route] NNN cut (or refresh) this run's worktree for a ticket from the remote; prints its path
18000
18243
  crew unassign [route] NNN hand back a session's ticket \u2014 clears assignee, next cycle picks it up
18001
18244
  crew sync [route] fast-forward the checkout and its worktrees from the remote
@@ -18270,7 +18513,7 @@ if (command === "passenger-sync-daemon") {
18270
18513
  } else {
18271
18514
  const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
18272
18515
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
18273
- if (configless && !existsSync35(findConfigFile(CREW_HOME))) {
18516
+ if (configless && !existsSync36(findConfigFile(CREW_HOME))) {
18274
18517
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
18275
18518
  } else {
18276
18519
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -18508,8 +18751,8 @@ if (command === "passenger-sync-daemon") {
18508
18751
  }
18509
18752
  if (host === "linux" && hasSystemd) {
18510
18753
  const label = labelFor(CREW_HOME, "run");
18511
- const staleTimer = join38(homedir10(), ".config", "systemd", "user", `${label}.timer`);
18512
- if (existsSync35(staleTimer)) {
18754
+ const staleTimer = join39(homedir10(), ".config", "systemd", "user", `${label}.timer`);
18755
+ if (existsSync36(staleTimer)) {
18513
18756
  process.stdout.write(
18514
18757
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
18515
18758
  `
@@ -18536,7 +18779,7 @@ if (command === "passenger-sync-daemon") {
18536
18779
  const tracker = new Tracker(c, cfg.ship);
18537
18780
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
18538
18781
  for (const r of await resolvedRepos(c)) {
18539
- if (!existsSync35(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
18782
+ if (!existsSync36(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
18540
18783
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
18541
18784
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
18542
18785
  }
@@ -18554,7 +18797,7 @@ if (command === "passenger-sync-daemon") {
18554
18797
  }
18555
18798
  async function doctorFix(o) {
18556
18799
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
18557
- const unitExists = (p) => existsSync35(p);
18800
+ const unitExists = (p) => existsSync36(p);
18558
18801
  const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
18559
18802
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
18560
18803
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -18571,7 +18814,7 @@ if (command === "passenger-sync-daemon") {
18571
18814
  for (const a of plan.actions) {
18572
18815
  if (a.kind === "enable") {
18573
18816
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
18574
- writeFileSync23(cfg.configFile, planEnable(readFileSync31(cfg.configFile, "utf8"), a.route));
18817
+ writeFileSync24(cfg.configFile, planEnable(readFileSync32(cfg.configFile, "utf8"), a.route));
18575
18818
  enabledSound.add(a.route);
18576
18819
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
18577
18820
  } else {
@@ -19675,7 +19918,7 @@ ${cu.tail.trimEnd()}
19675
19918
  process.exit(1);
19676
19919
  }
19677
19920
  requireArmed("run the daemon loop");
19678
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join38(CREW_HOME, "src"), join38(CREW_HOME, "dist"), join38(CREW_HOME, "bin")];
19921
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join39(CREW_HOME, "src"), join39(CREW_HOME, "dist"), join39(CREW_HOME, "bin")];
19679
19922
  const configPaths = daemonConfigPaths(cfg);
19680
19923
  watchPaths.push(...configPaths);
19681
19924
  noteDaemonStarted(cfg.ship.stateDir);
@@ -19904,10 +20147,10 @@ ${cu.tail.trimEnd()}
19904
20147
  `);
19905
20148
  if (!dryRun) {
19906
20149
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
19907
- const raw = existsSync35(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
20150
+ const raw = existsSync36(resolvedPath) ? JSON.parse(readFileSync32(resolvedPath, "utf8")) : {};
19908
20151
  raw.agentSkills = result.agentSkills;
19909
- mkdirSync22(dirOf(resolvedPath), { recursive: true });
19910
- writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
20152
+ mkdirSync23(dirOf2(resolvedPath), { recursive: true });
20153
+ writeFileSync24(resolvedPath, `${JSON.stringify(raw, null, 2)}
19911
20154
  `);
19912
20155
  }
19913
20156
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -19930,7 +20173,7 @@ ${cu.tail.trimEnd()}
19930
20173
  throw e;
19931
20174
  }
19932
20175
  const oldName = cfg.ship.name;
19933
- writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), newName));
20176
+ writeFileSync24(cfg.configFile, setShipNameInYaml(readFileSync32(cfg.configFile, "utf8"), newName));
19934
20177
  cfg.ship.name = newName;
19935
20178
  let renameMachineId;
19936
20179
  try {
@@ -19977,7 +20220,7 @@ ${cu.tail.trimEnd()}
19977
20220
  process.exit(2);
19978
20221
  };
19979
20222
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
19980
- const configText = readFileSync31(cfg.configFile, "utf8");
20223
+ const configText = readFileSync32(cfg.configFile, "utf8");
19981
20224
  if (sub === "check") {
19982
20225
  try {
19983
20226
  const routeArg = value("route") ?? positional[2];
@@ -20008,16 +20251,16 @@ ${cu.tail.trimEnd()}
20008
20251
  try {
20009
20252
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
20010
20253
  const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
20011
- if (!existsSync35(dir)) throw new ReposError(`${dir} does not exist on disk`);
20012
- const files = { exists: (r) => existsSync35(resolvePath(dir, r)), list: (r) => {
20254
+ if (!existsSync36(dir)) throw new ReposError(`${dir} does not exist on disk`);
20255
+ const files = { exists: (r) => existsSync36(resolvePath(dir, r)), list: (r) => {
20013
20256
  try {
20014
- return readdirSync16(resolvePath(dir, r));
20257
+ return readdirSync17(resolvePath(dir, r));
20015
20258
  } catch {
20016
20259
  return [];
20017
20260
  }
20018
20261
  }, read: (r) => {
20019
20262
  try {
20020
- return readFileSync31(resolvePath(dir, r), "utf8");
20263
+ return readFileSync32(resolvePath(dir, r), "utf8");
20021
20264
  } catch {
20022
20265
  return void 0;
20023
20266
  }
@@ -20059,19 +20302,19 @@ ${cu.tail.trimEnd()}
20059
20302
  let cleared;
20060
20303
  if (clear2) {
20061
20304
  const plan = clearRouteHooks(configText, a.route, a.name);
20062
- if (plan.removed.length > 0) writeFileSync23(cfg.configFile, plan.text);
20305
+ if (plan.removed.length > 0) writeFileSync24(cfg.configFile, plan.text);
20063
20306
  cleared = { file: cfg.configFile, removed: plan.removed };
20064
20307
  }
20065
20308
  if (write && suggestion.source === null && recipeIds.length === 0 && hookSpecs.length === 0) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
20066
20309
  if (write === "route") {
20067
20310
  const plan = planRouteHooks(configText, a.route, a.name, picked.hooks, { replace: flag("replace") });
20068
- writeFileSync23(cfg.configFile, plan.text);
20311
+ writeFileSync24(cfg.configFile, plan.text);
20069
20312
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept, replaced: plan.replaced };
20070
20313
  } else if (write === "repo") {
20071
20314
  const have = existingRepoConfig(files);
20072
20315
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
20073
20316
  const file = resolvePath(dir, ".crew.yaml");
20074
- writeFileSync23(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
20317
+ writeFileSync24(file, starterRepoConfig(picked.hooks, picked.release), { flag: "wx" });
20075
20318
  written = { target: "repo", file, hooks: Object.keys(picked.hooks), kept: [], replaced: [] };
20076
20319
  }
20077
20320
  if (flag("json")) {
@@ -20127,7 +20370,7 @@ ${cu.tail.trimEnd()}
20127
20370
  if (sub === "list" && !routeName) usage2();
20128
20371
  if (sub === "add") {
20129
20372
  try {
20130
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync35(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
20373
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync36(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
20131
20374
  routeName = a.route;
20132
20375
  addPath = a.path;
20133
20376
  addName = a.name;
@@ -20156,16 +20399,16 @@ ${cu.tail.trimEnd()}
20156
20399
  }
20157
20400
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
20158
20401
  let resolvedRemotes;
20159
- if (existsSync35(resolvedFile)) {
20402
+ if (existsSync36(resolvedFile)) {
20160
20403
  try {
20161
- resolvedRemotes = JSON.parse(readFileSync31(resolvedFile, "utf8"));
20404
+ resolvedRemotes = JSON.parse(readFileSync32(resolvedFile, "utf8"));
20162
20405
  } catch {
20163
20406
  }
20164
20407
  }
20165
20408
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
20166
20409
  const text = configText;
20167
20410
  const base = dirname20(cfg.configFile);
20168
- const isCheckout = (d) => existsSync35(`${d}/.git`);
20411
+ const isCheckout = (d) => existsSync36(`${d}/.git`);
20169
20412
  try {
20170
20413
  if (sub === "list") {
20171
20414
  if (flag("json")) {
@@ -20175,11 +20418,11 @@ ${cu.tail.trimEnd()}
20175
20418
  base,
20176
20419
  resolved: resolvedRemotes,
20177
20420
  originOf,
20178
- exists: (d) => existsSync35(d),
20421
+ exists: (d) => existsSync36(d),
20179
20422
  isCheckout,
20180
20423
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
20181
20424
  try {
20182
- return Object.keys(parseRepoConfig(readFileSync31(resolvePath(d, f), "utf8"), f).hooks).length > 0;
20425
+ return Object.keys(parseRepoConfig(readFileSync32(resolvePath(d, f), "utf8"), f).hooks).length > 0;
20183
20426
  } catch {
20184
20427
  return false;
20185
20428
  }
@@ -20196,7 +20439,7 @@ ${cu.tail.trimEnd()}
20196
20439
  for (const r of repos) {
20197
20440
  const origin2 = originOf(r.dir);
20198
20441
  const row = matchRepoName(origin2, resolvedRemotes);
20199
- const note = !existsSync35(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
20442
+ const note = !existsSync36(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
20200
20443
  process.stdout.write(`${r.name} ${r.dir} ${note}
20201
20444
  `);
20202
20445
  }
@@ -20208,9 +20451,9 @@ ${cu.tail.trimEnd()}
20208
20451
  ` : `${human}
20209
20452
  `);
20210
20453
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
20211
- if (existsSync35(plan2.target) && (existsSync35(`${plan2.target}/.git`) || readdirSync16(plan2.target).length > 0)) {
20454
+ if (existsSync36(plan2.target) && (existsSync36(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
20212
20455
  throw new ReposError(
20213
- existsSync35(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
20456
+ existsSync36(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
20214
20457
  );
20215
20458
  }
20216
20459
  if (dryRun) {
@@ -20228,14 +20471,14 @@ ${cu.tail.trimEnd()}
20228
20471
  throw new ReposError(`${e.message}${authHint}
20229
20472
  crew.yaml was not changed`);
20230
20473
  }
20231
- writeFileSync23(cfg.configFile, plan2.text);
20474
+ writeFileSync24(cfg.configFile, plan2.text);
20232
20475
  if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
20233
20476
  `);
20234
20477
  say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
20235
20478
  break;
20236
20479
  }
20237
20480
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
20238
- const top = existsSync35(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
20481
+ const top = existsSync36(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
20239
20482
  if (!top) throw new ReposError(`${target} is not a git checkout`);
20240
20483
  const origin = originOf(top);
20241
20484
  const plan = planRepoAdd({
@@ -20287,15 +20530,15 @@ ${cu.tail.trimEnd()}
20287
20530
  const created = await rowTracker.createRepoRow(rowPayload);
20288
20531
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
20289
20532
  `);
20290
- if (existsSync35(resolvedFile)) {
20291
- const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
20533
+ if (existsSync36(resolvedFile)) {
20534
+ const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
20292
20535
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
20293
20536
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
20294
- writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
20537
+ writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
20295
20538
  `);
20296
20539
  }
20297
20540
  }
20298
- writeFileSync23(cfg.configFile, plan.text);
20541
+ writeFileSync24(cfg.configFile, plan.text);
20299
20542
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
20300
20543
  `);
20301
20544
  } catch (e) {
@@ -20662,7 +20905,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20662
20905
  process.exit(2);
20663
20906
  }
20664
20907
  if (found.shipNameAdopted && !dryRun) {
20665
- writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), found.shipNameAdopted));
20908
+ writeFileSync24(cfg.configFile, setShipNameInYaml(readFileSync32(cfg.configFile, "utf8"), found.shipNameAdopted));
20666
20909
  cfg.ship.name = found.shipNameAdopted;
20667
20910
  }
20668
20911
  if (found.shipRow) {
@@ -20705,10 +20948,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20705
20948
  }
20706
20949
  }
20707
20950
  let previous;
20708
- const hadPreviousFile = existsSync35(resolvedPath);
20951
+ const hadPreviousFile = existsSync36(resolvedPath);
20709
20952
  if (hadPreviousFile) {
20710
20953
  try {
20711
- previous = JSON.parse(readFileSync31(resolvedPath, "utf8"));
20954
+ previous = JSON.parse(readFileSync32(resolvedPath, "utf8"));
20712
20955
  } catch {
20713
20956
  }
20714
20957
  }
@@ -20837,10 +21080,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20837
21080
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
20838
21081
  `);
20839
21082
  } else {
20840
- mkdirSync22(dirname20(resolvedPath), { recursive: true });
21083
+ mkdirSync23(dirname20(resolvedPath), { recursive: true });
20841
21084
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
20842
21085
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
20843
- writeFileSync23(tmpPath, `${JSON.stringify(resolved, null, 2)}
21086
+ writeFileSync24(tmpPath, `${JSON.stringify(resolved, null, 2)}
20844
21087
  `);
20845
21088
  renameSync3(tmpPath, resolvedPath);
20846
21089
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -20886,8 +21129,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20886
21129
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
20887
21130
  `);
20888
21131
  } else {
20889
- mkdirSync22(dirname20(apiKeyFile), { recursive: true });
20890
- writeFileSync23(apiKeyFile, `${apiKeyVar}=${apiKey}
21132
+ mkdirSync23(dirname20(apiKeyFile), { recursive: true });
21133
+ writeFileSync24(apiKeyFile, `${apiKeyVar}=${apiKey}
20891
21134
  `, { mode: 384 });
20892
21135
  process.stderr.write(`API key written to ${apiKeyFile}
20893
21136
  `);
@@ -21422,14 +21665,142 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21422
21665
  }
21423
21666
  case "periscope": {
21424
21667
  const subP = positional[1];
21425
- if (subP !== "up" && subP !== "start") {
21668
+ if (subP !== "up" && subP !== "start" && subP !== "down" && subP !== "end" && subP !== "list") {
21426
21669
  process.stderr.write("crew periscope up [route] REPO --topic TEXT [--isolate] [--host H] [--assignee ID] [--dry-run] [--json]\n");
21670
+ process.stderr.write("crew periscope down [route] NNN --keep|--discard [--allow-empty] [--json]\n");
21671
+ process.stderr.write("crew periscope list [route] [--json]\n");
21427
21672
  process.exit(2);
21428
21673
  }
21429
21674
  const jsonP = flag("json");
21430
21675
  const say = (event, message, data = {}) => process.stderr.write(jsonP ? `${JSON.stringify({ ...data, event, message })}
21431
21676
  ` : `${message}
21432
21677
  `);
21678
+ if (subP === "down" || subP === "end" || subP === "list") {
21679
+ try {
21680
+ const downArgs = subP === "list" ? void 0 : parsePeriscopeDownArgs(positional, { keep: flag("keep"), discard: flag("discard"), allowEmpty: flag("allow-empty"), json: jsonP });
21681
+ const routeName = subP === "list" ? positional[2] : downArgs.route;
21682
+ const routesP = (routeName ? [findRoute(cfg, routeName)] : cfg.routes.filter((r) => r.enabled)).filter((r) => r.resolved?.periscopesModelId);
21683
+ if (routesP.length === 0) throw new PeriscopeError("no connected route has a Periscopes table");
21684
+ const gather = async (rt) => {
21685
+ const tr = new Tracker(rt, cfg.ship);
21686
+ const repos = await resolvedRepos(rt);
21687
+ return (await tr.periscopeRows()).map(({ row, ticket }) => {
21688
+ const repo = repos.find((r) => r.id && r.id === ticket[tr.contract.columns.repo]) ?? (repos.length === 1 ? repos[0] : void 0);
21689
+ const str = (v) => typeof v === "string" ? v : "";
21690
+ return {
21691
+ id: row.id,
21692
+ ticketRecordId: ticket.id,
21693
+ ticket: ticket.issue_tag ?? ticket.issue_id,
21694
+ number: /(\d+)$/.exec(ticket.issue_id)?.[1] ?? "",
21695
+ repo: repo?.name ?? "?",
21696
+ state: str(row.state),
21697
+ url: str(row.url),
21698
+ worktree: str(row.worktree),
21699
+ branch: str(row.branch),
21700
+ lastActivityAt: str(row.last_activity_at) || null
21701
+ };
21702
+ });
21703
+ };
21704
+ if (subP === "list") {
21705
+ const entries = periscopeListEntries(
21706
+ (await Promise.all(routesP.map(gather))).flat(),
21707
+ /* @__PURE__ */ new Date(),
21708
+ (r) => pidsInWorktree(r.worktree, Number(r.number)).length > 0
21709
+ );
21710
+ process.stdout.write(jsonP ? `${JSON.stringify(entries)}
21711
+ ` : `${formatPeriscopeList(entries)}
21712
+ `);
21713
+ break;
21714
+ }
21715
+ const a = downArgs;
21716
+ const hits = [];
21717
+ for (const rt of routesP) {
21718
+ const rows = (await gather(rt)).filter((r) => r.number === a.number);
21719
+ if (rows.length) hits.push({ rt, rows });
21720
+ }
21721
+ if (hits.length !== 1) {
21722
+ throw new PeriscopeError(hits.length === 0 ? `no periscope for ticket ${a.number} on this ship` : `ticket ${a.number} has periscopes on more than one route (${hits.map((h) => h.rt.route).join(", ")}) \u2014 name the route`);
21723
+ }
21724
+ const { rt: routeD, rows: rowsD } = hits[0];
21725
+ const trD = new Tracker(routeD, cfg.ship);
21726
+ const cD = trD.contract;
21727
+ const reposD = await resolvedRepos(routeD);
21728
+ const repoD = reposD.find((r) => r.name === rowsD[0].repo);
21729
+ if (!repoD) throw new PeriscopeError(`cannot tell which repository ${rowsD[0].ticket} belongs to`);
21730
+ const { remote, base } = repoD.config.branch;
21731
+ const memberD = routeD.resolved.seats.pair ?? routeD.resolved.operator;
21732
+ const closedD = cD.statuses.resolved.includes("closed_obsolete") ? "closed_obsolete" : cD.statuses.wontFix;
21733
+ const depsD = {
21734
+ rowsForTicket: async () => rowsD,
21735
+ hasChanges: (wt) => status(wt).length > 0,
21736
+ commitAll: (wt, msg) => {
21737
+ git(wt, ["add", "-A"]);
21738
+ git(wt, ["commit", "-m", msg]);
21739
+ },
21740
+ push: (wt, branch) => {
21741
+ pushBranchUpstream(wt, remote, branch);
21742
+ return { sha: headSha(wt), remote };
21743
+ },
21744
+ changedFiles: (wt) => {
21745
+ const out = gitOk(wt, ["diff", "--numstat", `${remote}/${base}...HEAD`]) ?? "";
21746
+ return out.split("\n").filter(Boolean).map((l) => {
21747
+ const [add, rem, ...p] = l.split(" ");
21748
+ return { path: p.join(" "), added: Number(add) || 0, removed: Number(rem) || 0 };
21749
+ });
21750
+ },
21751
+ readTicket: async (id) => {
21752
+ const t = await trD.ticket(id);
21753
+ return { description: String(t.description ?? ""), hasProject: !!t[cD.columns.slice], hasRepo: !!t[cD.columns.repo] };
21754
+ },
21755
+ updateTicket: async (id, p) => {
21756
+ const patch2 = {};
21757
+ if (p.status === "new") {
21758
+ patch2[cD.columns.status] = "new";
21759
+ patch2[cD.columns.assignee] = null;
21760
+ if (p.description !== void 0) patch2.description = p.description;
21761
+ if (p.fillProjectAndRepo) {
21762
+ if (routeD.resolved.areaId) patch2[cD.columns.slice] = routeD.resolved.areaId;
21763
+ if (repoD.id) patch2[cD.columns.repo] = repoD.id;
21764
+ }
21765
+ } else {
21766
+ patch2[cD.columns.status] = closedD;
21767
+ patch2[cD.columns.resolutionNote] = p.note;
21768
+ }
21769
+ await trD.updateTicket(id, patch2);
21770
+ },
21771
+ postPushed: (id, sha, rem, branch) => trD.postEvent(id, pushedEventBody(sha, rem, branch), memberD, "pushed"),
21772
+ stopServer: async (wt, n) => {
21773
+ const pids = pidsInWorktree(wt, Number(n));
21774
+ if (pids.length) await killGently(pids);
21775
+ return pids.length;
21776
+ },
21777
+ removeWorktree: (wt, branch, deleteBranch) => {
21778
+ gitOk(repoD.dir, ["worktree", "remove", "--force", wt]);
21779
+ if (deleteBranch) gitOk(repoD.dir, ["branch", "-D", branch]);
21780
+ },
21781
+ setRowState: (id, state2) => trD.updatePeriscope(id, { state: state2 }),
21782
+ forgetLocal: (id) => removePeriscopeSidecar(cfg.ship.stateDir, id),
21783
+ say
21784
+ };
21785
+ if (dryRun) {
21786
+ say("dry-run", `would end the periscope for ${rowsD[0].ticket} (${a.mode})`);
21787
+ break;
21788
+ }
21789
+ const res = await runPeriscopeDown(depsD, a);
21790
+ if (jsonP) process.stdout.write(`${JSON.stringify({ event: "done", ...res })}
21791
+ `);
21792
+ else process.stdout.write(`${res.ticket} ${res.state}
21793
+ `);
21794
+ } catch (e) {
21795
+ if (e instanceof PeriscopeError || e instanceof ConfigError) {
21796
+ process.stderr.write(`crew periscope: ${e.message}
21797
+ `);
21798
+ process.exit(1);
21799
+ }
21800
+ throw e;
21801
+ }
21802
+ break;
21803
+ }
21433
21804
  try {
21434
21805
  const a = parsePeriscopeUpArgs(positional, { topic: value("topic"), host: value("host"), assignee: value("assignee"), isolate: flag("isolate"), json: jsonP });
21435
21806
  const candidatesP = a.route ? [findRoute(cfg, a.route)] : cfg.routes.filter((r) => r.enabled);
@@ -21493,10 +21864,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21493
21864
  },
21494
21865
  prepare: async (path) => {
21495
21866
  for (const f of repoP.config.worktrees.copy) {
21496
- const src = join38(repoP.dir, f);
21497
- if (!existsSync35(src)) continue;
21498
- mkdirSync22(dirOf(join38(path, f)), { recursive: true });
21499
- copyFileSync2(src, join38(path, f));
21867
+ const src = join39(repoP.dir, f);
21868
+ if (!existsSync36(src)) continue;
21869
+ mkdirSync23(dirOf2(join39(path, f)), { recursive: true });
21870
+ copyFileSync2(src, join39(path, f));
21500
21871
  }
21501
21872
  const setup = repoP.config.hooks.setup;
21502
21873
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -21508,6 +21879,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21508
21879
  return script ? runScript(script, { cwd, env, shell: shellP, timeoutMs: name === "preview" ? 18e4 : void 0 }) : null;
21509
21880
  },
21510
21881
  createRow: (fields) => trackerP.createPeriscope(fields),
21882
+ recordLocal: (sc) => writePeriscopeSidecar(cfg.ship.stateDir, sc),
21511
21883
  comment: (ticketId, body) => trackerP.postComment(ticketId, body, memberId),
21512
21884
  teardown: async (path, branch) => {
21513
21885
  const n = Number(path.match(/(\d+)\/?$/)?.[1]);
@@ -21719,7 +22091,7 @@ ${res.url}
21719
22091
  };
21720
22092
  const droppedRoutes = [];
21721
22093
  try {
21722
- const rawCfg = parseYaml2(readFileSync31(cfg.configFile, "utf8"));
22094
+ const rawCfg = parseYaml2(readFileSync32(cfg.configFile, "utf8"));
21723
22095
  for (const r of rawCfg?.routes ?? []) {
21724
22096
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
21725
22097
  try {
@@ -21761,8 +22133,8 @@ ${res.url}
21761
22133
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
21762
22134
  break;
21763
22135
  }
21764
- mkdirSync22(cfg.ship.stateDir, { recursive: true });
21765
- writeFileSync23(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
22136
+ mkdirSync23(cfg.ship.stateDir, { recursive: true });
22137
+ writeFileSync24(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
21766
22138
  `);
21767
22139
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
21768
22140
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -21813,7 +22185,7 @@ ${res.url}
21813
22185
  }
21814
22186
  case "log": {
21815
22187
  try {
21816
- const text = readFileSync31(cfg.ship.logFile, "utf8").split("\n");
22188
+ const text = readFileSync32(cfg.ship.logFile, "utf8").split("\n");
21817
22189
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
21818
22190
  `);
21819
22191
  } catch {
@@ -21836,7 +22208,7 @@ ${res.url}
21836
22208
  };
21837
22209
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
21838
22210
  const tracker = new Tracker(route, cfg.ship);
21839
- const configText = readFileSync31(cfg.configFile, "utf8");
22211
+ const configText = readFileSync32(cfg.configFile, "utf8");
21840
22212
  const cfgBase = dirname20(cfg.configFile);
21841
22213
  const configured = configuredRepos(configText, route.route, cfgBase);
21842
22214
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
@@ -21865,7 +22237,7 @@ ${res.url}
21865
22237
  }
21866
22238
  if (configured.some((r) => r.dir === dir)) {
21867
22239
  try {
21868
- writeFileSync23(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
22240
+ writeFileSync24(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
21869
22241
  } catch (e) {
21870
22242
  if (e instanceof ReposError) fail(e.message);
21871
22243
  throw e;
@@ -21873,29 +22245,29 @@ ${res.url}
21873
22245
  }
21874
22246
  if (row) {
21875
22247
  await tracker.deleteRepoRow(row.id);
21876
- if (existsSync35(resolvedFile)) {
21877
- const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
22248
+ if (existsSync36(resolvedFile)) {
22249
+ const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
21878
22250
  if (raw.repoNames) delete raw.repoNames[row.id];
21879
22251
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
21880
- writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
22252
+ writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
21881
22253
  `);
21882
22254
  }
21883
22255
  }
21884
- if (existsSync35(join38(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
22256
+ if (existsSync36(join39(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync16(dir, { recursive: true, force: true });
21885
22257
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
21886
22258
  break;
21887
22259
  }
21888
22260
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
21889
- const fresh = !existsSync35(join38(dir, ".git"));
21890
- if (existsSync35(dir) && fresh && readdirSync16(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
22261
+ const fresh = !existsSync36(join39(dir, ".git"));
22262
+ if (existsSync36(dir) && fresh && readdirSync17(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
21891
22263
  if (dryRun) {
21892
22264
  say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
21893
22265
  break;
21894
22266
  }
21895
22267
  if (fresh) {
21896
- mkdirSync22(dir, { recursive: true });
21897
- writeFileSync23(join38(dir, "README.md"), SAMPLE_README);
21898
- writeFileSync23(join38(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
22268
+ mkdirSync23(dir, { recursive: true });
22269
+ writeFileSync24(join39(dir, "README.md"), SAMPLE_README);
22270
+ writeFileSync24(join39(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
21899
22271
  const g = (...a) => {
21900
22272
  const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
21901
22273
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -21913,17 +22285,17 @@ ${res.url}
21913
22285
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
21914
22286
  });
21915
22287
  repoId = (await tracker.createRepoRow(payload)).id;
21916
- if (existsSync35(resolvedFile)) {
21917
- const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
22288
+ if (existsSync36(resolvedFile)) {
22289
+ const raw = JSON.parse(readFileSync32(resolvedFile, "utf8"));
21918
22290
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
21919
22291
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
21920
- writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
22292
+ writeFileSync24(resolvedFile, `${JSON.stringify(raw, null, 2)}
21921
22293
  `);
21922
22294
  }
21923
22295
  }
21924
22296
  if (!configured.some((r) => r.dir === dir)) {
21925
22297
  try {
21926
- writeFileSync23(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
22298
+ writeFileSync24(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
21927
22299
  } catch (e) {
21928
22300
  if (e instanceof ReposError) fail(e.message);
21929
22301
  throw e;
@@ -22045,6 +22417,7 @@ ${res.url}
22045
22417
  const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
22046
22418
  const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
22047
22419
  const runs = liveRuns(cfg.ship.stateDir);
22420
+ const periscopes = await periscopesForStatus(cfg.ship.stateDir);
22048
22421
  if (flag("json")) {
22049
22422
  const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
22050
22423
  const repos = await resolvedRepos(route);
@@ -22109,6 +22482,8 @@ ${res.url}
22109
22482
  },
22110
22483
  // Additive (CREW-2822): every live run on this ship. `version` stays 1.
22111
22484
  runs: runs.map((x) => describeRun(x, route, seatIds)),
22485
+ // Additive (CREW-2857): every live periscope on this ship, from its local record. `version` stays 1.
22486
+ periscopes,
22112
22487
  watermark: state.watermark(),
22113
22488
  route: {
22114
22489
  name: route.route,
@@ -22183,7 +22558,8 @@ ${owed.map((l) => ` ${l}
22183
22558
  route: ${route.route} -> ${route.dir}
22184
22559
  enabled: ${route.enabled}
22185
22560
  periscopes: ${periscopeSummary(cfg)}
22186
- crew: ${state.isPaused() ? "paused" : "active"}
22561
+ ` + periscopes.map((p) => `periscope: ${p.ticket} ${p.repo} ${p.url} ${p.state}${p.serving ? "" : " (not serving)"} expires ${p.expiresAt}
22562
+ `).join("") + `crew: ${state.isPaused() ? "paused" : "active"}
22187
22563
  release: ${state.releaseSkipReason() ? "paused" : "active"}
22188
22564
  watermark: ${state.watermark()}
22189
22565
  `
@@ -22254,7 +22630,7 @@ watermark: ${state.watermark()}
22254
22630
  }
22255
22631
  for (const r of repos) {
22256
22632
  const remote = r.config.branch.remote;
22257
- if (existsSync35(r.dir)) {
22633
+ if (existsSync36(r.dir)) {
22258
22634
  pushAuth.push({
22259
22635
  route: c.route,
22260
22636
  repo: r.name,
@@ -22301,7 +22677,12 @@ watermark: ${state.watermark()}
22301
22677
  }
22302
22678
  for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
22303
22679
  const previewProblem2 = await previewHostProblem(cfg);
22304
- if (previewProblem2) problems.push({ severity: "blocker", kind: "preview_host", message: previewProblem2 });
22680
+ if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
22681
+ if (cfg.routes.some((x) => x.route === route.route && x.enabled && x.hostPeriscopes)) {
22682
+ const { settled } = await settleRepos(reposOf(route), (t) => t.name, (t) => resolveOneRepo(route, t));
22683
+ const tablePresent = flag("offline") ? void 0 : (await new Tracker(route, cfg.ship).periscopesStatus().catch(() => ({ present: true }))).present;
22684
+ problems.push(...routePeriscopeProblems({ route: route.route, hosts: true, repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview })), tablePresent }));
22685
+ }
22305
22686
  problems.push(...attentionProblems(openShipAttention(state), problems));
22306
22687
  if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
22307
22688
  process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows, problems, passed }, null, 2)}
@@ -22319,13 +22700,14 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
22319
22700
  `);
22320
22701
  const previewProblem = await previewHostProblem(cfg);
22321
22702
  if (previewProblem) {
22322
- process.stdout.write(` BLOCKER ${previewProblem}
22703
+ const blocks = previewHostFinding(previewProblem).severity === "blocker";
22704
+ process.stdout.write(` ${blocks ? "BLOCKER" : "ATTENTION"} ${previewProblem}
22323
22705
  `);
22324
- process.exitCode = 1;
22706
+ if (blocks) process.exitCode = 1;
22325
22707
  }
22326
22708
  try {
22327
22709
  const evFile = eventFileFor(cfg.ship.stateDir);
22328
- const evSize = existsSync35(evFile) ? statSync10(evFile).size : 0;
22710
+ const evSize = existsSync36(evFile) ? statSync10(evFile).size : 0;
22329
22711
  process.stdout.write(
22330
22712
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
22331
22713
  ` : "events log: ok\n"
@@ -22336,7 +22718,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
22336
22718
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
22337
22719
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
22338
22720
  try {
22339
- return readFileSync31(p, "utf8");
22721
+ return readFileSync32(p, "utf8");
22340
22722
  } catch {
22341
22723
  return null;
22342
22724
  }
@@ -22427,6 +22809,18 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
22427
22809
  `
22428
22810
  );
22429
22811
  const periscopes = await tracker.periscopesStatus();
22812
+ if (cfg.routes.some((x) => x.route === route.route && x.enabled && x.hostPeriscopes)) {
22813
+ const { settled } = await settleRepos(reposOf(route), (t) => t.name, (t) => resolveOneRepo(route, t));
22814
+ for (const f of routePeriscopeProblems({
22815
+ route: route.route,
22816
+ hosts: true,
22817
+ tablePresent: periscopes.present,
22818
+ repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview }))
22819
+ })) {
22820
+ process.stdout.write(`periscopes: ${f.severity.toUpperCase()} ${f.message}
22821
+ `);
22822
+ }
22823
+ }
22430
22824
  process.stdout.write(
22431
22825
  !periscopes.present ? "periscopes: Periscopes table absent \u2014 this workspace cannot host periscopes\n" : periscopes.missing.length === 0 ? `periscopes: Periscopes table present (${periscopes.modelId})
22432
22826
  ` : `periscopes: Periscopes table present (${periscopes.modelId}) but missing ${periscopes.missing.join(", ")} \u2014 update the Issues template