@tablation/crew 0.81.0 → 0.82.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 +315 -237
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6479,7 +6479,7 @@ var Tracker = class {
6479
6479
  * `beatIdle` in a `finally` — the same shape as `setCrewStatus`'s
6480
6480
  * working/idle pair, one level up (ship, not seat).
6481
6481
  */
6482
- async beatEngaged(shipName2, connectionName, ticketRecordId) {
6482
+ async beatEngaged(shipName2, connectionName, ticketRecordId, since2) {
6483
6483
  const row = await this.myShipRow(shipName2);
6484
6484
  if (!row) return;
6485
6485
  await this.client.records.update(this.route.resolved.shipsModelId, row.id, {
@@ -6487,7 +6487,7 @@ var Tracker = class {
6487
6487
  host: hostname(),
6488
6488
  pid: process.pid,
6489
6489
  engaged: true,
6490
- engaged_since: (/* @__PURE__ */ new Date()).toISOString(),
6490
+ engaged_since: since2 ?? (/* @__PURE__ */ new Date()).toISOString(),
6491
6491
  engaged_connection: connectionName,
6492
6492
  engaged_ticket_id: ticketRecordId,
6493
6493
  ...this.attentionPatch(row)
@@ -8561,6 +8561,8 @@ function planAgentRun(o) {
8561
8561
  eventsPath: join19(streamsDir, `${base}.events.jsonl`),
8562
8562
  ticket: o.ticket,
8563
8563
  ticketRecordId: o.ticketRecordId ?? null,
8564
+ repo: o.repo ?? null,
8565
+ routeName: o.route.route,
8564
8566
  agentLog,
8565
8567
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
8566
8568
  promptSha
@@ -8975,6 +8977,8 @@ function spawnAgent(plan, emit, hintMoved) {
8975
8977
  route: plan.route,
8976
8978
  ticket: plan.ticket ?? null,
8977
8979
  ticketRecordId: plan.ticketRecordId ?? null,
8980
+ repo: plan.repo ?? null,
8981
+ routeName: plan.routeName,
8978
8982
  model: plan.model,
8979
8983
  toolchain: id.toolchain,
8980
8984
  harness: id.harness,
@@ -9546,7 +9550,7 @@ function doctorSelftestLines(stored, toolchains) {
9546
9550
  }
9547
9551
 
9548
9552
  // src/cli.ts
9549
- import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename12 } from "path";
9553
+ import { resolve as resolve5, dirname as dirname20, join as join38, basename as basename12 } from "path";
9550
9554
  import { fileURLToPath } from "url";
9551
9555
  import { hostname as hostname2, homedir as homedir10 } from "os";
9552
9556
  import { spawnSync as spawnSync7 } from "child_process";
@@ -13379,6 +13383,164 @@ Passed: ${passed}.${willClose ? " Closing this alert." : ""}`,
13379
13383
  return out;
13380
13384
  }
13381
13385
 
13386
+ // src/live-runs.ts
13387
+ import { existsSync as existsSync24, readdirSync as readdirSync10 } from "fs";
13388
+ import { join as join28 } from "path";
13389
+
13390
+ // src/interrupted.ts
13391
+ import { existsSync as existsSync23, readdirSync as readdirSync9, readFileSync as readFileSync21 } from "fs";
13392
+ import { join as join27 } from "path";
13393
+ var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
13394
+ function interruptedComment(b) {
13395
+ 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.";
13396
+ return `${INTERRUPTED_MARKER}
13397
+
13398
+ The agent run on this ticket was cut short (${b.reason}) before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The next run on this seat resumes from the worktree rather than starting over.`;
13399
+ }
13400
+ var handled = /* @__PURE__ */ new Set();
13401
+ var pidAlive = (pid) => {
13402
+ try {
13403
+ process.kill(pid, 0);
13404
+ return true;
13405
+ } catch (e) {
13406
+ return e.code === "EPERM";
13407
+ }
13408
+ };
13409
+ function runFinished(streamPath) {
13410
+ const path = `${streamPath}.meta.json`;
13411
+ if (!existsSync23(path)) return false;
13412
+ try {
13413
+ return JSON.parse(readFileSync21(path, "utf8")).running !== true;
13414
+ } catch {
13415
+ return true;
13416
+ }
13417
+ }
13418
+ function orphanedStreams(o) {
13419
+ const dir = join27(o.stateDir, "streams");
13420
+ if (!existsSync23(dir)) return [];
13421
+ const ownerAlive = o.isOwnerAlive ?? pidAlive;
13422
+ const out = [];
13423
+ for (const name of readdirSync9(dir)) {
13424
+ if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13425
+ const streamPath = join27(dir, name);
13426
+ if (runFinished(streamPath)) continue;
13427
+ const sidecar = readRunSidecar(streamPath);
13428
+ if (!sidecar || sidecar.route !== o.routeLabel) continue;
13429
+ if (ownerAlive(sidecar.ownerPid)) continue;
13430
+ out.push({ streamPath, sidecar });
13431
+ }
13432
+ return out;
13433
+ }
13434
+ async function reconcileRuns(o) {
13435
+ let adopted = 0;
13436
+ let interrupted = 0;
13437
+ for (const { streamPath, sidecar } of orphanedStreams(o)) {
13438
+ if (handled.has(streamPath)) continue;
13439
+ handled.add(streamPath);
13440
+ const isAlive = o.isAlive ?? runIsAlive;
13441
+ const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents, o.roleConcurrency?.[sidecar.role] ?? 1) : null;
13442
+ const ticketLock = isAlive(sidecar) && sidecar.ticketRecordId ? o.state.acquireTicket(sidecar.ticketRecordId) : null;
13443
+ const work = (async () => {
13444
+ try {
13445
+ const result = await reattachRun(streamPath, sidecar, o.emit, {
13446
+ agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
13447
+ isAlive: o.isAlive,
13448
+ pollMs: o.pollMs,
13449
+ onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
13450
+ });
13451
+ if (result.adopted) adopted++;
13452
+ else interrupted++;
13453
+ try {
13454
+ await o.afterRun?.(sidecar, result);
13455
+ } catch (e) {
13456
+ o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
13457
+ }
13458
+ } catch (e) {
13459
+ o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
13460
+ } finally {
13461
+ if (lock?.ok) lock.release();
13462
+ if (ticketLock?.ok) ticketLock.release();
13463
+ }
13464
+ })();
13465
+ if (isAlive(sidecar)) {
13466
+ if (o.inFlight) {
13467
+ o.inFlight.add(work);
13468
+ void work.finally(() => o.inFlight.delete(work));
13469
+ }
13470
+ } else {
13471
+ await work;
13472
+ }
13473
+ }
13474
+ return { adopted, interrupted };
13475
+ }
13476
+
13477
+ // src/live-runs.ts
13478
+ function liveRuns(stateDir, isAlive = runIsAlive) {
13479
+ const dir = join28(stateDir, "streams");
13480
+ if (!existsSync24(dir)) return [];
13481
+ const out = [];
13482
+ for (const name of readdirSync10(dir)) {
13483
+ if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13484
+ const streamPath = join28(dir, name);
13485
+ if (runFinished(streamPath)) continue;
13486
+ const sidecar = readRunSidecar(streamPath);
13487
+ if (!sidecar || !isAlive(sidecar)) continue;
13488
+ out.push({ ...sidecar, streamPath });
13489
+ }
13490
+ return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
13491
+ }
13492
+ function siblingRuns(runs, ownTicketRecordId) {
13493
+ return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
13494
+ }
13495
+ async function settleRunEnd(o) {
13496
+ const { tracker, emit } = o;
13497
+ const all = o.runs ?? liveRuns(o.stateDir);
13498
+ const slug = routeSlug(o.route.route);
13499
+ const seatSiblings = siblingRuns(all.filter((r) => r.role === o.role && r.route === slug), o.ownTicketRecordId);
13500
+ const shipSiblings = siblingRuns(all, o.ownTicketRecordId);
13501
+ const newest = (rs) => rs[rs.length - 1];
13502
+ if (o.memberId) {
13503
+ try {
13504
+ if (seatSiblings.length) await tracker.setCrewStatus(o.memberId, "working", newest(seatSiblings).ticketRecordId);
13505
+ else await tracker.setCrewStatus(o.memberId, "idle");
13506
+ } catch (e) {
13507
+ emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
13508
+ }
13509
+ }
13510
+ try {
13511
+ if (shipSiblings.length) {
13512
+ const n = newest(shipSiblings);
13513
+ await tracker.beatEngaged(o.shipName, n.routeName ?? n.route, n.ticketRecordId, n.spawnedAt);
13514
+ } else {
13515
+ await tracker.beatIdle(o.shipName);
13516
+ }
13517
+ } catch (e) {
13518
+ emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
13519
+ }
13520
+ }
13521
+ function describeRun(run2, route, seatIds, worktreeOf = (d, n) => {
13522
+ try {
13523
+ return worktreeForNumber(d, n)?.path ?? null;
13524
+ } catch {
13525
+ return null;
13526
+ }
13527
+ }) {
13528
+ const ownRoute = run2.route === routeSlug(route.route);
13529
+ const repoDir = run2.repo ? reposOf(route).find((t) => t.name === run2.repo)?.dir : void 0;
13530
+ const num = run2.ticket?.replace(/^\D+/, "") ?? "";
13531
+ return {
13532
+ role: run2.role,
13533
+ seat: ownRoute ? seatIds[run2.role] ?? null : null,
13534
+ ticket: run2.ticket,
13535
+ ticketRecordId: run2.ticketRecordId,
13536
+ repo: run2.repo ?? null,
13537
+ worktree: repoDir && num ? worktreeOf(repoDir, num) : null,
13538
+ stream: run2.streamPath,
13539
+ pid: run2.pid,
13540
+ startedAt: run2.spawnedAt
13541
+ };
13542
+ }
13543
+
13382
13544
  // src/projects-cmd.ts
13383
13545
  async function buildProjectsReport(routes, open) {
13384
13546
  return Promise.all(routes.map(async (r) => {
@@ -13588,7 +13750,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
13588
13750
  }
13589
13751
 
13590
13752
  // src/watch.ts
13591
- import { watch as watchFile, existsSync as existsSync23, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
13753
+ import { watch as watchFile, existsSync as existsSync25, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
13592
13754
  var E = "\x1B[";
13593
13755
  var dim = (s) => `${E}2m${s}${E}0m`;
13594
13756
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -13689,7 +13851,7 @@ function renderHeaders(states, now = Date.now()) {
13689
13851
  return states.map((s) => renderHeader(s, now)).join("");
13690
13852
  }
13691
13853
  function readFrom(file, from) {
13692
- if (!existsSync23(file)) return { events: [], offset: from };
13854
+ if (!existsSync25(file)) return { events: [], offset: from };
13693
13855
  const size = statSync7(file).size;
13694
13856
  if (size < from) return readFrom(file, 0);
13695
13857
  if (size === from) return { events: [], offset: from };
@@ -13932,17 +14094,17 @@ function nextRoles(decision, ran) {
13932
14094
  }
13933
14095
 
13934
14096
  // src/ship-name.ts
13935
- import { existsSync as existsSync25, mkdirSync as mkdirSync16, readFileSync as readFileSync22, renameSync as renameSync2, writeFileSync as writeFileSync18 } from "fs";
14097
+ import { existsSync as existsSync27, mkdirSync as mkdirSync16, readFileSync as readFileSync23, renameSync as renameSync2, writeFileSync as writeFileSync18 } from "fs";
13936
14098
  import { dirname as dirname14 } from "path";
13937
14099
  import { parseDocument } from "yaml";
13938
14100
 
13939
14101
  // src/machine-id.ts
13940
14102
  import { execFileSync as execFileSync4 } from "child_process";
13941
14103
  import { createHash as createHash4 } from "crypto";
13942
- import { existsSync as existsSync24, mkdirSync as mkdirSync15, readFileSync as readFileSync21, writeFileSync as writeFileSync17 } from "fs";
13943
- import { dirname as dirname13, join as join27 } from "path";
14104
+ import { existsSync as existsSync26, mkdirSync as mkdirSync15, readFileSync as readFileSync22, writeFileSync as writeFileSync17 } from "fs";
14105
+ import { dirname as dirname13, join as join29 } from "path";
13944
14106
  function machineIdPathFor(stateDir) {
13945
- return join27(stateDir, "machine-id");
14107
+ return join29(stateDir, "machine-id");
13946
14108
  }
13947
14109
  var sha256 = (s) => createHash4("sha256").update(s).digest("hex");
13948
14110
  function readRawMachineId(platform2 = process.platform) {
@@ -13955,8 +14117,8 @@ function readRawMachineId(platform2 = process.platform) {
13955
14117
  return /"IOPlatformUUID"\s*=\s*"([^"]+)"/.exec(out)?.[1];
13956
14118
  }
13957
14119
  for (const p of ["/etc/machine-id", "/var/lib/dbus/machine-id"]) {
13958
- if (existsSync24(p)) {
13959
- const id = readFileSync21(p, "utf8").trim();
14120
+ if (existsSync26(p)) {
14121
+ const id = readFileSync22(p, "utf8").trim();
13960
14122
  if (id) return id;
13961
14123
  }
13962
14124
  }
@@ -13967,8 +14129,8 @@ function readRawMachineId(platform2 = process.platform) {
13967
14129
  function ensureMachineId(stateDir, opts = {}) {
13968
14130
  if (opts.override?.trim()) return sha256(opts.override.trim());
13969
14131
  const path = machineIdPathFor(stateDir);
13970
- if (existsSync24(path)) {
13971
- const cached = readFileSync21(path, "utf8").trim();
14132
+ if (existsSync26(path)) {
14133
+ const cached = readFileSync22(path, "utf8").trim();
13972
14134
  if (/^[0-9a-f]{64}$/.test(cached)) return cached;
13973
14135
  }
13974
14136
  const raw = (opts.read ?? readRawMachineId)();
@@ -14046,19 +14208,19 @@ async function syncRouteName(i) {
14046
14208
  return { status: "ok", ...base, action, name: i.local };
14047
14209
  }
14048
14210
  function readSyncedName(resolvedFile) {
14049
- if (!existsSync25(resolvedFile)) return void 0;
14211
+ if (!existsSync27(resolvedFile)) return void 0;
14050
14212
  try {
14051
- const v = JSON.parse(readFileSync22(resolvedFile, "utf8")).shipName;
14213
+ const v = JSON.parse(readFileSync23(resolvedFile, "utf8")).shipName;
14052
14214
  return typeof v === "string" ? v : void 0;
14053
14215
  } catch {
14054
14216
  return void 0;
14055
14217
  }
14056
14218
  }
14057
14219
  function writeSyncedName(resolvedFile, name) {
14058
- if (!existsSync25(resolvedFile)) return;
14220
+ if (!existsSync27(resolvedFile)) return;
14059
14221
  let raw;
14060
14222
  try {
14061
- raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
14223
+ raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
14062
14224
  } catch {
14063
14225
  return;
14064
14226
  }
@@ -14073,7 +14235,7 @@ function writeSyncedName(resolvedFile, name) {
14073
14235
  async function reconcileShipName(i) {
14074
14236
  let local = i.ship.name;
14075
14237
  try {
14076
- local = shipNameOfYaml(readFileSync22(i.configFile, "utf8")) ?? local;
14238
+ local = shipNameOfYaml(readFileSync23(i.configFile, "utf8")) ?? local;
14077
14239
  } catch {
14078
14240
  }
14079
14241
  const resolvedFile = resolvedPathFor(i.stateDir, i.route);
@@ -14097,7 +14259,7 @@ async function reconcileShipName(i) {
14097
14259
  }
14098
14260
  if (result.status !== "ok") return result;
14099
14261
  if (result.action === "adopt" || result.action === "row-wins") {
14100
- const text = readFileSync22(i.configFile, "utf8");
14262
+ const text = readFileSync23(i.configFile, "utf8");
14101
14263
  writeFileSync18(i.configFile, setShipNameInYaml(text, result.name));
14102
14264
  i.ship.name = result.name;
14103
14265
  if (result.action === "row-wins") {
@@ -14743,7 +14905,7 @@ function describeCapability(i) {
14743
14905
  }
14744
14906
 
14745
14907
  // src/agents-sync-run.ts
14746
- import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync23, writeFileSync as writeFileSync19 } from "fs";
14908
+ import { existsSync as existsSync28, mkdirSync as mkdirSync17, readFileSync as readFileSync24, writeFileSync as writeFileSync19 } from "fs";
14747
14909
  import { dirname as dirname15 } from "path";
14748
14910
  async function runAgentsSync(target, o) {
14749
14911
  const dryRun = o.dryRun ?? false;
@@ -14777,7 +14939,7 @@ async function runAgentsSync(target, o) {
14777
14939
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
14778
14940
  if (!dryRun) {
14779
14941
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
14780
- const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
14942
+ const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync24(resolvedPath, "utf8")) : {};
14781
14943
  raw.agentPersonas = personaResult.agentPersonas;
14782
14944
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
14783
14945
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
@@ -14886,7 +15048,7 @@ async function showLogEntry(route, entryId, opts = {}) {
14886
15048
 
14887
15049
  // src/repos-check.ts
14888
15050
  import { spawnSync as spawnSync6 } from "child_process";
14889
- import { readFileSync as readFileSync24 } from "fs";
15051
+ import { readFileSync as readFileSync25 } from "fs";
14890
15052
  import { basename as basename8 } from "path";
14891
15053
  var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
14892
15054
  function checkHookSyntax(script, shell) {
@@ -14927,7 +15089,7 @@ function checkRepoSpec(dir, opts = {}) {
14927
15089
  }
14928
15090
  let parsed;
14929
15091
  try {
14930
- parsed = parseRepoConfig(readFileSync24(file, "utf8"), file);
15092
+ parsed = parseRepoConfig(readFileSync25(file, "utf8"), file);
14931
15093
  } catch (e) {
14932
15094
  if (!(e instanceof RepoConfigError)) throw e;
14933
15095
  return { ...empty, file, error: e.message, exitCode: 1 };
@@ -14984,7 +15146,7 @@ import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml"
14984
15146
 
14985
15147
  // src/repos-cmd.ts
14986
15148
  import { homedir as homedir9 } from "os";
14987
- import { basename as basename9, join as join28, resolve as resolve3 } from "path";
15149
+ import { basename as basename9, join as join30, resolve as resolve3 } from "path";
14988
15150
  import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
14989
15151
  var ReposError = class extends Error {
14990
15152
  };
@@ -15262,7 +15424,7 @@ function planRepoClone(i) {
15262
15424
  }
15263
15425
  const ship = doc.get("ship", true);
15264
15426
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
15265
- const target = i.dir ? expandDir(i.dir, i.base) : join28(expandDir(basePath, i.base), i.route.split("/")[0], name);
15427
+ const target = i.dir ? expandDir(i.dir, i.base) : join30(expandDir(basePath, i.base), i.route.split("/")[0], name);
15266
15428
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
15267
15429
  return { ...add, remote, target };
15268
15430
  }
@@ -15661,7 +15823,7 @@ ${run2.err}` });
15661
15823
  }
15662
15824
 
15663
15825
  // src/sample-ticket.ts
15664
- import { basename as basename10, dirname as dirname17, join as join29 } from "path";
15826
+ import { basename as basename10, dirname as dirname17, join as join31 } from "path";
15665
15827
  var SAMPLE_REPO = "crew-sample";
15666
15828
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
15667
15829
  var SAMPLE_MARKER = "sample: true";
@@ -15684,8 +15846,8 @@ has something safe to work on: its test and build hooks always pass, it has no
15684
15846
  remote, and \`crew sample-ticket --remove\` deletes it.
15685
15847
  `;
15686
15848
  function sampleDir(existing, reposBase, workspace) {
15687
- if (existing.length > 0) return join29(dirname17(existing[0]), SAMPLE_REPO);
15688
- return join29(reposBase, workspace, SAMPLE_REPO);
15849
+ if (existing.length > 0) return join31(dirname17(existing[0]), SAMPLE_REPO);
15850
+ return join31(reposBase, workspace, SAMPLE_REPO);
15689
15851
  }
15690
15852
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
15691
15853
  function sampleDescription() {
@@ -15777,8 +15939,8 @@ function schedulerInstalled(plan, exists, crontab) {
15777
15939
  }
15778
15940
 
15779
15941
  // src/worktree-sweep.ts
15780
- import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync9 } from "fs";
15781
- import { basename as basename11, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
15942
+ import { existsSync as existsSync29, readFileSync as readFileSync26, readdirSync as readdirSync11 } from "fs";
15943
+ import { basename as basename11, dirname as dirname18, join as join32, resolve as resolve4 } from "path";
15782
15944
  function planWorktreeSweep(target, tickets, contract) {
15783
15945
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
15784
15946
  const verified = contract.statuses.verified;
@@ -15837,7 +15999,7 @@ function siblingWorktreeDirs(target) {
15837
15999
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
15838
16000
  let names;
15839
16001
  try {
15840
- names = readdirSync9(parent);
16002
+ names = readdirSync11(parent);
15841
16003
  } catch {
15842
16004
  return [];
15843
16005
  }
@@ -15845,7 +16007,7 @@ function siblingWorktreeDirs(target) {
15845
16007
  for (const name of names) {
15846
16008
  if (!name.startsWith(stem)) continue;
15847
16009
  const m = DIR_NUMBER_SUFFIXED.exec(name);
15848
- const path = join30(parent, name);
16010
+ const path = join32(parent, name);
15849
16011
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
15850
16012
  }
15851
16013
  return out;
@@ -15880,7 +16042,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
15880
16042
  let removed = 0;
15881
16043
  let keptBranches = 0;
15882
16044
  for (const a of actions) {
15883
- if (!dryRun && base && !a.keepBranch && existsSync27(a.path) && isHandBackMerge(a.path, base)) {
16045
+ if (!dryRun && base && !a.keepBranch && existsSync29(a.path) && isHandBackMerge(a.path, base)) {
15884
16046
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
15885
16047
  log.emit(
15886
16048
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -15888,7 +16050,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
15888
16050
  );
15889
16051
  }
15890
16052
  }
15891
- if (!dryRun && existsSync27(a.path)) {
16053
+ if (!dryRun && existsSync29(a.path)) {
15892
16054
  let dirty = false;
15893
16055
  try {
15894
16056
  dirty = status(a.path).length > 0;
@@ -15899,7 +16061,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
15899
16061
  continue;
15900
16062
  }
15901
16063
  }
15902
- if (!dryRun && a.requirePushed && a.branch && existsSync27(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
16064
+ if (!dryRun && a.requirePushed && a.branch && existsSync29(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
15903
16065
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
15904
16066
  continue;
15905
16067
  }
@@ -15948,7 +16110,7 @@ function isHandBackMerge(path, base) {
15948
16110
  if (!msgPath) return false;
15949
16111
  let first;
15950
16112
  try {
15951
- first = readFileSync25(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
16113
+ first = readFileSync26(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
15952
16114
  } catch {
15953
16115
  return false;
15954
16116
  }
@@ -16053,14 +16215,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
16053
16215
  }
16054
16216
 
16055
16217
  // src/stream-sweep.ts
16056
- import { existsSync as existsSync28, readdirSync as readdirSync10, rmSync as rmSync13, statSync as statSync8 } from "fs";
16057
- import { join as join31 } from "path";
16218
+ import { existsSync as existsSync30, readdirSync as readdirSync12, rmSync as rmSync13, statSync as statSync8 } from "fs";
16219
+ import { join as join33 } from "path";
16058
16220
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
16059
16221
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
16060
- const dir = join31(stateDir, "streams");
16061
- if (!existsSync28(dir)) return [];
16222
+ const dir = join33(stateDir, "streams");
16223
+ if (!existsSync30(dir)) return [];
16062
16224
  const cutoff = now - retentionDays * DAY_MS2;
16063
- return readdirSync10(dir).map((name) => join31(dir, name)).filter((path) => {
16225
+ return readdirSync12(dir).map((name) => join33(dir, name)).filter((path) => {
16064
16226
  try {
16065
16227
  return statSync8(path).mtimeMs < cutoff;
16066
16228
  } catch {
@@ -16086,9 +16248,9 @@ function applyStreamSweep(paths, dryRun, log) {
16086
16248
  return removed;
16087
16249
  }
16088
16250
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16089
- if (!existsSync28(stateDir)) return [];
16251
+ if (!existsSync30(stateDir)) return [];
16090
16252
  const cutoff = now - retentionDays * DAY_MS2;
16091
- return readdirSync10(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join31(stateDir, name)).filter((path) => {
16253
+ return readdirSync12(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join33(stateDir, name)).filter((path) => {
16092
16254
  try {
16093
16255
  return statSync8(path).mtimeMs < cutoff;
16094
16256
  } catch {
@@ -16098,23 +16260,23 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16098
16260
  }
16099
16261
 
16100
16262
  // src/cli.ts
16101
- import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync15 } from "fs";
16263
+ 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";
16102
16264
  import { createInterface } from "readline/promises";
16103
16265
  import { dirname as dirOf, resolve as resolvePath } from "path";
16104
16266
 
16105
16267
  // src/ssh-keys.ts
16106
16268
  import { execFileSync as execFileSync6 } from "child_process";
16107
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync26, chmodSync } from "fs";
16108
- import { dirname as dirname19, join as join32 } from "path";
16269
+ import { existsSync as existsSync31, mkdirSync as mkdirSync18, readFileSync as readFileSync27, chmodSync } from "fs";
16270
+ import { dirname as dirname19, join as join34 } from "path";
16109
16271
  var SshKeyError = class extends Error {
16110
16272
  };
16111
16273
  function shipSshKeyPathFor(stateDir) {
16112
- return join32(stateDir, "ssh", "id_ed25519");
16274
+ return join34(stateDir, "ssh", "id_ed25519");
16113
16275
  }
16114
16276
  function ensureShipSshKeypair(stateDir) {
16115
16277
  const privateKeyPath = shipSshKeyPathFor(stateDir);
16116
16278
  const publicKeyPath = `${privateKeyPath}.pub`;
16117
- if (!existsSync29(privateKeyPath)) {
16279
+ if (!existsSync31(privateKeyPath)) {
16118
16280
  mkdirSync18(dirname19(privateKeyPath), { recursive: true });
16119
16281
  try {
16120
16282
  execFileSync6("ssh-keygen", [
@@ -16135,10 +16297,10 @@ function ensureShipSshKeypair(stateDir) {
16135
16297
  }
16136
16298
  chmodSync(privateKeyPath, 384);
16137
16299
  }
16138
- if (!existsSync29(publicKeyPath)) {
16300
+ if (!existsSync31(publicKeyPath)) {
16139
16301
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
16140
16302
  }
16141
- const publicKey = readFileSync26(publicKeyPath, "utf8").trim();
16303
+ const publicKey = readFileSync27(publicKeyPath, "utf8").trim();
16142
16304
  return { privateKeyPath, publicKeyPath, publicKey };
16143
16305
  }
16144
16306
  function sshKeygenAvailable() {
@@ -16355,9 +16517,9 @@ function syncAllPassengerCheckouts(cfg) {
16355
16517
 
16356
16518
  // src/tunnel.ts
16357
16519
  import { spawn as spawn5 } from "child_process";
16358
- import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync27, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync11 } from "fs";
16520
+ import { existsSync as existsSync32, mkdirSync as mkdirSync19, readFileSync as readFileSync28, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync13 } from "fs";
16359
16521
  import { randomInt } from "crypto";
16360
- import { join as join33 } from "path";
16522
+ import { join as join35 } from "path";
16361
16523
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
16362
16524
  var defaultSpawn = (cmd, args) => spawn5(cmd, args, DEFAULT_SPAWN_OPTIONS);
16363
16525
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -16424,20 +16586,20 @@ function startTunnel(opts) {
16424
16586
  };
16425
16587
  }
16426
16588
  function tunnelStatePath(stateDir, workspaceId) {
16427
- return join33(stateDir, "passengers", `${workspaceId}.json`);
16589
+ return join35(stateDir, "passengers", `${workspaceId}.json`);
16428
16590
  }
16429
16591
  function readPersistedTunnel(stateDir, workspaceId) {
16430
16592
  const path = tunnelStatePath(stateDir, workspaceId);
16431
- if (!existsSync30(path)) return void 0;
16593
+ if (!existsSync32(path)) return void 0;
16432
16594
  try {
16433
- return JSON.parse(readFileSync27(path, "utf8"));
16595
+ return JSON.parse(readFileSync28(path, "utf8"));
16434
16596
  } catch {
16435
16597
  return void 0;
16436
16598
  }
16437
16599
  }
16438
16600
  function writePersistedTunnel(stateDir, state) {
16439
16601
  const path = tunnelStatePath(stateDir, state.workspaceId);
16440
- mkdirSync19(join33(stateDir, "passengers"), { recursive: true });
16602
+ mkdirSync19(join35(stateDir, "passengers"), { recursive: true });
16441
16603
  writeFileSync20(path, `${JSON.stringify(state, null, 2)}
16442
16604
  `);
16443
16605
  }
@@ -16535,9 +16697,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
16535
16697
  }
16536
16698
  }
16537
16699
  function readAllPersistedWorkspaceIds(stateDir) {
16538
- const dir = join33(stateDir, "passengers");
16539
- if (!existsSync30(dir)) return [];
16540
- return readdirSync11(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
16700
+ const dir = join35(stateDir, "passengers");
16701
+ if (!existsSync32(dir)) return [];
16702
+ return readdirSync13(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
16541
16703
  }
16542
16704
 
16543
16705
  // src/mcp-endpoint-registry.ts
@@ -16657,8 +16819,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
16657
16819
  import { createServer as createHttpServer } from "http";
16658
16820
  import { spawn as spawn6 } from "child_process";
16659
16821
  import { randomBytes } from "crypto";
16660
- import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync12 } from "fs";
16661
- import { join as join34 } from "path";
16822
+ import { existsSync as existsSync33, mkdirSync as mkdirSync20, readFileSync as readFileSync29, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync14 } from "fs";
16823
+ import { join as join36 } from "path";
16662
16824
 
16663
16825
  // src/secret-compare.ts
16664
16826
  import { timingSafeEqual } from "crypto";
@@ -16747,20 +16909,20 @@ var defaultSpawn2 = (cmd, args, env) => {
16747
16909
  return child;
16748
16910
  };
16749
16911
  function syncDaemonStatePath(stateDir, workspaceId) {
16750
- return join34(stateDir, "passengers", `${workspaceId}-sync.json`);
16912
+ return join36(stateDir, "passengers", `${workspaceId}-sync.json`);
16751
16913
  }
16752
16914
  function readPersistedSyncDaemon(stateDir, workspaceId) {
16753
16915
  const path = syncDaemonStatePath(stateDir, workspaceId);
16754
- if (!existsSync31(path)) return void 0;
16916
+ if (!existsSync33(path)) return void 0;
16755
16917
  try {
16756
- return JSON.parse(readFileSync28(path, "utf8"));
16918
+ return JSON.parse(readFileSync29(path, "utf8"));
16757
16919
  } catch {
16758
16920
  return void 0;
16759
16921
  }
16760
16922
  }
16761
16923
  function writePersistedSyncDaemon(stateDir, state) {
16762
16924
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
16763
- mkdirSync20(join34(stateDir, "passengers"), { recursive: true });
16925
+ mkdirSync20(join36(stateDir, "passengers"), { recursive: true });
16764
16926
  writeFileSync21(path, `${JSON.stringify(state, null, 2)}
16765
16927
  `);
16766
16928
  }
@@ -16781,10 +16943,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
16781
16943
  }
16782
16944
  }
16783
16945
  function readAllPersistedSyncWorkspaceIds(stateDir) {
16784
- const dir = join34(stateDir, "passengers");
16785
- if (!existsSync31(dir)) return [];
16946
+ const dir = join36(stateDir, "passengers");
16947
+ if (!existsSync33(dir)) return [];
16786
16948
  const suffix = "-sync.json";
16787
- return readdirSync12(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
16949
+ return readdirSync14(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
16788
16950
  }
16789
16951
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
16790
16952
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -16870,98 +17032,9 @@ function describeCause(err) {
16870
17032
  }
16871
17033
 
16872
17034
  // src/daemon.ts
16873
- import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync14, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
16874
- import { join as join36 } from "path";
17035
+ import { existsSync as existsSync34, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync15, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
17036
+ import { join as join37 } from "path";
16875
17037
  import { randomBytes as randomBytes2 } from "crypto";
16876
-
16877
- // src/interrupted.ts
16878
- import { existsSync as existsSync32, readdirSync as readdirSync13, readFileSync as readFileSync29 } from "fs";
16879
- import { join as join35 } from "path";
16880
- var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
16881
- function interruptedComment(b) {
16882
- 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.";
16883
- return `${INTERRUPTED_MARKER}
16884
-
16885
- The agent run on this ticket was cut short (${b.reason}) before it handed on${b.branch ? `; its branch is \`${b.branch}\`` : ""}. ${tree} The next run on this seat resumes from the worktree rather than starting over.`;
16886
- }
16887
- var handled = /* @__PURE__ */ new Set();
16888
- var pidAlive = (pid) => {
16889
- try {
16890
- process.kill(pid, 0);
16891
- return true;
16892
- } catch (e) {
16893
- return e.code === "EPERM";
16894
- }
16895
- };
16896
- function runFinished(streamPath) {
16897
- const path = `${streamPath}.meta.json`;
16898
- if (!existsSync32(path)) return false;
16899
- try {
16900
- return JSON.parse(readFileSync29(path, "utf8")).running !== true;
16901
- } catch {
16902
- return true;
16903
- }
16904
- }
16905
- function orphanedStreams(o) {
16906
- const dir = join35(o.stateDir, "streams");
16907
- if (!existsSync32(dir)) return [];
16908
- const ownerAlive = o.isOwnerAlive ?? pidAlive;
16909
- const out = [];
16910
- for (const name of readdirSync13(dir)) {
16911
- if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
16912
- const streamPath = join35(dir, name);
16913
- if (runFinished(streamPath)) continue;
16914
- const sidecar = readRunSidecar(streamPath);
16915
- if (!sidecar || sidecar.route !== o.routeLabel) continue;
16916
- if (ownerAlive(sidecar.ownerPid)) continue;
16917
- out.push({ streamPath, sidecar });
16918
- }
16919
- return out;
16920
- }
16921
- async function reconcileRuns(o) {
16922
- let adopted = 0;
16923
- let interrupted = 0;
16924
- for (const { streamPath, sidecar } of orphanedStreams(o)) {
16925
- if (handled.has(streamPath)) continue;
16926
- handled.add(streamPath);
16927
- const isAlive = o.isAlive ?? runIsAlive;
16928
- const lock = isAlive(sidecar) ? o.state.acquireRun(sidecar.role, o.maxConcurrentAgents, o.roleConcurrency?.[sidecar.role] ?? 1) : null;
16929
- const ticketLock = isAlive(sidecar) && sidecar.ticketRecordId ? o.state.acquireTicket(sidecar.ticketRecordId) : null;
16930
- const work = (async () => {
16931
- try {
16932
- const result = await reattachRun(streamPath, sidecar, o.emit, {
16933
- agentLogKey: await o.agentLogKey?.(sidecar.role).catch(() => void 0),
16934
- isAlive: o.isAlive,
16935
- pollMs: o.pollMs,
16936
- onAdopted: () => o.emit.emit(`adopted ${sidecar.role} run (pid ${sidecar.pid}) left by a previous crew process`, { step: "agent", role: sidecar.role })
16937
- });
16938
- if (result.adopted) adopted++;
16939
- else interrupted++;
16940
- try {
16941
- await o.afterRun?.(sidecar, result);
16942
- } catch (e) {
16943
- o.emit.warn(`could not follow up on ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
16944
- }
16945
- } catch (e) {
16946
- o.emit.warn(`could not reattach ${sidecar.role} run ${streamPath}: ${e.message}`, { step: "agent" });
16947
- } finally {
16948
- if (lock?.ok) lock.release();
16949
- if (ticketLock?.ok) ticketLock.release();
16950
- }
16951
- })();
16952
- if (isAlive(sidecar)) {
16953
- if (o.inFlight) {
16954
- o.inFlight.add(work);
16955
- void work.finally(() => o.inFlight.delete(work));
16956
- }
16957
- } else {
16958
- await work;
16959
- }
16960
- }
16961
- return { adopted, interrupted };
16962
- }
16963
-
16964
- // src/daemon.ts
16965
17038
  var DEFAULT_FLOOR_MS = 5e3;
16966
17039
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
16967
17040
  var defaultSleep = (ms2) => new Promise((resolve6) => setTimeout(resolve6, ms2));
@@ -17030,8 +17103,8 @@ function latestMtime(path) {
17030
17103
  if (stat.isFile()) return stat.mtimeMs;
17031
17104
  if (!stat.isDirectory()) return 0;
17032
17105
  let latest = stat.mtimeMs;
17033
- for (const entry of readdirSync14(path, { withFileTypes: true })) {
17034
- latest = Math.max(latest, latestMtime(join36(path, entry.name)));
17106
+ for (const entry of readdirSync15(path, { withFileTypes: true })) {
17107
+ latest = Math.max(latest, latestMtime(join37(path, entry.name)));
17035
17108
  }
17036
17109
  return latest;
17037
17110
  }
@@ -17040,13 +17113,13 @@ function computeTreeSignature(paths) {
17040
17113
  }
17041
17114
  function makeStaleChecker(paths, opts = {}) {
17042
17115
  const initial = computeTreeSignature(paths);
17043
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync33(opts.reloadSentinel);
17116
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync34(opts.reloadSentinel);
17044
17117
  }
17045
17118
  function reloadSentinelFor(stateDir) {
17046
- return join36(stateDir, "reload-requested");
17119
+ return join37(stateDir, "reload-requested");
17047
17120
  }
17048
17121
  function daemonStampFor(stateDir) {
17049
- return join36(stateDir, "daemon.started");
17122
+ return join37(stateDir, "daemon.started");
17050
17123
  }
17051
17124
  function noteDaemonStarted(stateDir, now = Date.now()) {
17052
17125
  mkdirSync21(stateDir, { recursive: true });
@@ -17309,13 +17382,15 @@ async function reconcileForRoute(ctx, o) {
17309
17382
  await guardRunHandoff(route, ship, tracker, sc.role, sc.ticketRecordId, emit, sc.spawnedAt);
17310
17383
  }
17311
17384
  } finally {
17312
- if (memberId) {
17313
- await tracker.setCrewStatus(memberId, "idle").catch((e) => {
17314
- emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
17315
- });
17316
- }
17317
- await tracker.beatIdle(ship.name).catch((e) => {
17318
- emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
17385
+ await settleRunEnd({
17386
+ tracker,
17387
+ shipName: ship.name,
17388
+ route,
17389
+ role: sc.role,
17390
+ memberId,
17391
+ ownTicketRecordId: sc.ticketRecordId,
17392
+ stateDir: ship.stateDir,
17393
+ emit
17319
17394
  });
17320
17395
  }
17321
17396
  }
@@ -17469,18 +17544,16 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
17469
17544
  await spawnAgent(plan, emit, hintMoved);
17470
17545
  await guardRunHandoff(route, ship, tracker, role, workingId, emit, runStart);
17471
17546
  } finally {
17472
- if (memberId) {
17473
- try {
17474
- await tracker.setCrewStatus(memberId, "idle");
17475
- } catch (e) {
17476
- emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
17477
- }
17478
- }
17479
- try {
17480
- await tracker.beatIdle(ship.name);
17481
- } catch (e) {
17482
- emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
17483
- }
17547
+ await settleRunEnd({
17548
+ tracker,
17549
+ shipName: ship.name,
17550
+ route,
17551
+ role,
17552
+ memberId,
17553
+ ownTicketRecordId: workingId,
17554
+ stateDir: ship.stateDir,
17555
+ emit
17556
+ });
17484
17557
  }
17485
17558
  }
17486
17559
  async function buildEnvironment(route, ship, ticket) {
@@ -17922,7 +17995,7 @@ if (command === "passenger-sync-daemon") {
17922
17995
  } else {
17923
17996
  const toleratesNoRoutes = command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
17924
17997
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
17925
- if (configless && !existsSync34(findConfigFile(CREW_HOME))) {
17998
+ if (configless && !existsSync35(findConfigFile(CREW_HOME))) {
17926
17999
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
17927
18000
  } else {
17928
18001
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -18160,8 +18233,8 @@ if (command === "passenger-sync-daemon") {
18160
18233
  }
18161
18234
  if (host === "linux" && hasSystemd) {
18162
18235
  const label = labelFor(CREW_HOME, "run");
18163
- const staleTimer = join37(homedir10(), ".config", "systemd", "user", `${label}.timer`);
18164
- if (existsSync34(staleTimer)) {
18236
+ const staleTimer = join38(homedir10(), ".config", "systemd", "user", `${label}.timer`);
18237
+ if (existsSync35(staleTimer)) {
18165
18238
  process.stdout.write(
18166
18239
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
18167
18240
  `
@@ -18188,7 +18261,7 @@ if (command === "passenger-sync-daemon") {
18188
18261
  const tracker = new Tracker(c, cfg.ship);
18189
18262
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
18190
18263
  for (const r of await resolvedRepos(c)) {
18191
- if (!existsSync34(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
18264
+ if (!existsSync35(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
18192
18265
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
18193
18266
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
18194
18267
  }
@@ -18206,7 +18279,7 @@ if (command === "passenger-sync-daemon") {
18206
18279
  }
18207
18280
  async function doctorFix(o) {
18208
18281
  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))));
18209
- const unitExists = (p) => existsSync34(p);
18282
+ const unitExists = (p) => existsSync35(p);
18210
18283
  const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
18211
18284
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
18212
18285
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -18995,18 +19068,16 @@ ${cu.tail.trimEnd()}
18995
19068
  await guardRunHandoff(w.route, cfg.ship, fleetTracker, w.role, fleetWorkingId, wemit, runStart);
18996
19069
  } finally {
18997
19070
  dropLock();
18998
- if (fleetMemberId) {
18999
- try {
19000
- await fleetTracker.setCrewStatus(fleetMemberId, "idle");
19001
- } catch (e) {
19002
- wemit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
19003
- }
19004
- }
19005
- try {
19006
- await fleetTracker.beatIdle(cfg.ship.name);
19007
- } catch (e) {
19008
- wemit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
19009
- }
19071
+ await settleRunEnd({
19072
+ tracker: fleetTracker,
19073
+ shipName: cfg.ship.name,
19074
+ route: w.route,
19075
+ role: w.role,
19076
+ memberId: fleetMemberId,
19077
+ ownTicketRecordId: fleetWorkingId,
19078
+ stateDir: cfg.ship.stateDir,
19079
+ emit: wemit
19080
+ });
19010
19081
  }
19011
19082
  if (!skipInlineRelease) await releaseFleet();
19012
19083
  break;
@@ -19236,18 +19307,16 @@ ${cu.tail.trimEnd()}
19236
19307
  await guardRunHandoff(route, cfg.ship, tracker2, current, workingId, emit, runStart);
19237
19308
  } finally {
19238
19309
  dropLock();
19239
- if (memberId) {
19240
- try {
19241
- await tracker2.setCrewStatus(memberId, "idle");
19242
- } catch (e) {
19243
- emit.warn(`could not clear crew status: ${e.message}`, { step: "agent" });
19244
- }
19245
- }
19246
- try {
19247
- await tracker2.beatIdle(cfg.ship.name);
19248
- } catch (e) {
19249
- emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
19250
- }
19310
+ await settleRunEnd({
19311
+ tracker: tracker2,
19312
+ shipName: cfg.ship.name,
19313
+ route,
19314
+ role: current,
19315
+ memberId,
19316
+ ownTicketRecordId: workingId,
19317
+ stateDir: cfg.ship.stateDir,
19318
+ emit
19319
+ });
19251
19320
  }
19252
19321
  ran.push(current);
19253
19322
  if (ran.length >= maxRoles || explicitRole) break;
@@ -19331,7 +19400,7 @@ ${cu.tail.trimEnd()}
19331
19400
  process.exit(1);
19332
19401
  }
19333
19402
  requireArmed("run the daemon loop");
19334
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join37(CREW_HOME, "src"), join37(CREW_HOME, "dist"), join37(CREW_HOME, "bin")];
19403
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join38(CREW_HOME, "src"), join38(CREW_HOME, "dist"), join38(CREW_HOME, "bin")];
19335
19404
  const configPaths = daemonConfigPaths(cfg);
19336
19405
  watchPaths.push(...configPaths);
19337
19406
  noteDaemonStarted(cfg.ship.stateDir);
@@ -19560,7 +19629,7 @@ ${cu.tail.trimEnd()}
19560
19629
  `);
19561
19630
  if (!dryRun) {
19562
19631
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
19563
- const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
19632
+ const raw = existsSync35(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
19564
19633
  raw.agentSkills = result.agentSkills;
19565
19634
  mkdirSync22(dirOf(resolvedPath), { recursive: true });
19566
19635
  writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -19664,10 +19733,10 @@ ${cu.tail.trimEnd()}
19664
19733
  try {
19665
19734
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
19666
19735
  const dir = repoDirOf(configText, dirname20(cfg.configFile), a.route, a.name);
19667
- if (!existsSync34(dir)) throw new ReposError(`${dir} does not exist on disk`);
19668
- const files = { exists: (r) => existsSync34(resolvePath(dir, r)), list: (r) => {
19736
+ if (!existsSync35(dir)) throw new ReposError(`${dir} does not exist on disk`);
19737
+ const files = { exists: (r) => existsSync35(resolvePath(dir, r)), list: (r) => {
19669
19738
  try {
19670
- return readdirSync15(resolvePath(dir, r));
19739
+ return readdirSync16(resolvePath(dir, r));
19671
19740
  } catch {
19672
19741
  return [];
19673
19742
  }
@@ -19783,7 +19852,7 @@ ${cu.tail.trimEnd()}
19783
19852
  if (sub === "list" && !routeName) usage2();
19784
19853
  if (sub === "add") {
19785
19854
  try {
19786
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync34(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
19855
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync35(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
19787
19856
  routeName = a.route;
19788
19857
  addPath = a.path;
19789
19858
  addName = a.name;
@@ -19812,7 +19881,7 @@ ${cu.tail.trimEnd()}
19812
19881
  }
19813
19882
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
19814
19883
  let resolvedRemotes;
19815
- if (existsSync34(resolvedFile)) {
19884
+ if (existsSync35(resolvedFile)) {
19816
19885
  try {
19817
19886
  resolvedRemotes = JSON.parse(readFileSync31(resolvedFile, "utf8"));
19818
19887
  } catch {
@@ -19821,7 +19890,7 @@ ${cu.tail.trimEnd()}
19821
19890
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
19822
19891
  const text = configText;
19823
19892
  const base = dirname20(cfg.configFile);
19824
- const isCheckout = (d) => existsSync34(`${d}/.git`);
19893
+ const isCheckout = (d) => existsSync35(`${d}/.git`);
19825
19894
  try {
19826
19895
  if (sub === "list") {
19827
19896
  if (flag("json")) {
@@ -19831,7 +19900,7 @@ ${cu.tail.trimEnd()}
19831
19900
  base,
19832
19901
  resolved: resolvedRemotes,
19833
19902
  originOf,
19834
- exists: (d) => existsSync34(d),
19903
+ exists: (d) => existsSync35(d),
19835
19904
  isCheckout,
19836
19905
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
19837
19906
  try {
@@ -19852,7 +19921,7 @@ ${cu.tail.trimEnd()}
19852
19921
  for (const r of repos) {
19853
19922
  const origin2 = originOf(r.dir);
19854
19923
  const row = matchRepoName(origin2, resolvedRemotes);
19855
- const note = !existsSync34(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
19924
+ const note = !existsSync35(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
19856
19925
  process.stdout.write(`${r.name} ${r.dir} ${note}
19857
19926
  `);
19858
19927
  }
@@ -19864,9 +19933,9 @@ ${cu.tail.trimEnd()}
19864
19933
  ` : `${human}
19865
19934
  `);
19866
19935
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
19867
- if (existsSync34(plan2.target) && (existsSync34(`${plan2.target}/.git`) || readdirSync15(plan2.target).length > 0)) {
19936
+ if (existsSync35(plan2.target) && (existsSync35(`${plan2.target}/.git`) || readdirSync16(plan2.target).length > 0)) {
19868
19937
  throw new ReposError(
19869
- existsSync34(`${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`
19938
+ 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`
19870
19939
  );
19871
19940
  }
19872
19941
  if (dryRun) {
@@ -19891,7 +19960,7 @@ ${cu.tail.trimEnd()}
19891
19960
  break;
19892
19961
  }
19893
19962
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
19894
- const top = existsSync34(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
19963
+ const top = existsSync35(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
19895
19964
  if (!top) throw new ReposError(`${target} is not a git checkout`);
19896
19965
  const origin = originOf(top);
19897
19966
  const plan = planRepoAdd({
@@ -19943,7 +20012,7 @@ ${cu.tail.trimEnd()}
19943
20012
  const created = await rowTracker.createRepoRow(rowPayload);
19944
20013
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
19945
20014
  `);
19946
- if (existsSync34(resolvedFile)) {
20015
+ if (existsSync35(resolvedFile)) {
19947
20016
  const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
19948
20017
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
19949
20018
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
@@ -20360,7 +20429,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20360
20429
  }
20361
20430
  }
20362
20431
  let previous;
20363
- const hadPreviousFile = existsSync34(resolvedPath);
20432
+ const hadPreviousFile = existsSync35(resolvedPath);
20364
20433
  if (hadPreviousFile) {
20365
20434
  try {
20366
20435
  previous = JSON.parse(readFileSync31(resolvedPath, "utf8"));
@@ -21402,7 +21471,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21402
21471
  }
21403
21472
  if (row) {
21404
21473
  await tracker.deleteRepoRow(row.id);
21405
- if (existsSync34(resolvedFile)) {
21474
+ if (existsSync35(resolvedFile)) {
21406
21475
  const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
21407
21476
  if (raw.repoNames) delete raw.repoNames[row.id];
21408
21477
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
@@ -21410,21 +21479,21 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21410
21479
  `);
21411
21480
  }
21412
21481
  }
21413
- if (existsSync34(join37(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
21482
+ if (existsSync35(join38(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
21414
21483
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
21415
21484
  break;
21416
21485
  }
21417
21486
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
21418
- const fresh = !existsSync34(join37(dir, ".git"));
21419
- if (existsSync34(dir) && fresh && readdirSync15(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
21487
+ const fresh = !existsSync35(join38(dir, ".git"));
21488
+ if (existsSync35(dir) && fresh && readdirSync16(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
21420
21489
  if (dryRun) {
21421
21490
  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`);
21422
21491
  break;
21423
21492
  }
21424
21493
  if (fresh) {
21425
21494
  mkdirSync22(dir, { recursive: true });
21426
- writeFileSync23(join37(dir, "README.md"), SAMPLE_README);
21427
- writeFileSync23(join37(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
21495
+ writeFileSync23(join38(dir, "README.md"), SAMPLE_README);
21496
+ writeFileSync23(join38(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
21428
21497
  const g = (...a) => {
21429
21498
  const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
21430
21499
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -21442,7 +21511,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21442
21511
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
21443
21512
  });
21444
21513
  repoId = (await tracker.createRepoRow(payload)).id;
21445
- if (existsSync34(resolvedFile)) {
21514
+ if (existsSync35(resolvedFile)) {
21446
21515
  const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
21447
21516
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
21448
21517
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
@@ -21573,6 +21642,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21573
21642
  const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route.resolved?.operator ?? "")).catch(() => []);
21574
21643
  const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
21575
21644
  const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
21645
+ const runs = liveRuns(cfg.ship.stateDir);
21576
21646
  if (flag("json")) {
21577
21647
  const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
21578
21648
  const repos = await resolvedRepos(route);
@@ -21590,7 +21660,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21590
21660
  label: ROLE_LABEL[r],
21591
21661
  crewId: seatId ?? null,
21592
21662
  name: crewRow?.name ?? null,
21593
- personaName
21663
+ personaName,
21664
+ // Additive (CREW-2822): how many runs this seat has live right now.
21665
+ running: runs.filter((x) => x.role === r && x.route === routeSlug(route.route)).length
21594
21666
  };
21595
21667
  });
21596
21668
  process.stdout.write(`${JSON.stringify({
@@ -21633,6 +21705,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21633
21705
  roles: ROLE_NAMES.map((r) => ({ name: r, label: ROLE_LABEL[r], paused: state.isRolePaused(r) })),
21634
21706
  seats: seats2
21635
21707
  },
21708
+ // Additive (CREW-2822): every live run on this ship. `version` stays 1.
21709
+ runs: runs.map((x) => describeRun(x, route, seatIds)),
21636
21710
  watermark: state.watermark(),
21637
21711
  route: {
21638
21712
  name: route.route,
@@ -21745,6 +21819,10 @@ watermark: ${state.watermark()}
21745
21819
  `
21746
21820
  );
21747
21821
  }
21822
+ for (const x of runs) {
21823
+ process.stdout.write(`running: ${x.role} \u2014 ${x.ticket ?? "(no ticket)"}${x.repo ? ` in ${x.repo}` : ""} (pid ${x.pid}, since ${x.spawnedAt})
21824
+ `);
21825
+ }
21748
21826
  if (shipRows.length) {
21749
21827
  process.stdout.write("ships:\n");
21750
21828
  for (const s of shipRows) {
@@ -21773,7 +21851,7 @@ watermark: ${state.watermark()}
21773
21851
  }
21774
21852
  for (const r of repos) {
21775
21853
  const remote = r.config.branch.remote;
21776
- if (existsSync34(r.dir)) {
21854
+ if (existsSync35(r.dir)) {
21777
21855
  pushAuth.push({
21778
21856
  route: c.route,
21779
21857
  repo: r.name,
@@ -21834,7 +21912,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
21834
21912
  `);
21835
21913
  try {
21836
21914
  const evFile = eventFileFor(cfg.ship.stateDir);
21837
- const evSize = existsSync34(evFile) ? statSync10(evFile).size : 0;
21915
+ const evSize = existsSync35(evFile) ? statSync10(evFile).size : 0;
21838
21916
  process.stdout.write(
21839
21917
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
21840
21918
  ` : "events log: ok\n"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",