@tablation/crew 0.53.1 → 0.53.3

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 +189 -78
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -32,7 +32,7 @@ function doctorBaseReport(r, base, health) {
32
32
  }
33
33
 
34
34
  // src/cli.ts
35
- import { resolve as resolve4, dirname as dirname12, join as join25, basename as basename9 } from "path";
35
+ import { resolve as resolve5, dirname as dirname13, join as join26, basename as basename9 } from "path";
36
36
  import { fileURLToPath } from "url";
37
37
  import { hostname as hostname2, homedir as homedir8 } from "os";
38
38
  import { spawnSync as spawnSync6 } from "child_process";
@@ -8767,7 +8767,7 @@ function spawnAgent(plan, emit) {
8767
8767
  for (const w of plan.warnings ?? []) emit.warn(w, { step: "agent", role: plan.role });
8768
8768
  const eventsSink = openSink(plan.eventsPath);
8769
8769
  const id = identityOf(plan, adapter.harness);
8770
- return new Promise((resolve5, reject) => {
8770
+ return new Promise((resolve6, reject) => {
8771
8771
  let streamFd;
8772
8772
  try {
8773
8773
  mkdirSync6(dirname6(plan.streamPath), { recursive: true });
@@ -8826,7 +8826,7 @@ function spawnAgent(plan, emit) {
8826
8826
  await endSink(eventsSink);
8827
8827
  if (spawnFailed) return;
8828
8828
  const { ms: ms2 } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey);
8829
- resolve5({ code: code ?? 1, ms: ms2 });
8829
+ resolve6({ code: code ?? 1, ms: ms2 });
8830
8830
  });
8831
8831
  child.stdin?.on("error", (err) => {
8832
8832
  emit.warn(`agent closed stdin before the prompt was delivered: ${err.message}`, { step: "agent" });
@@ -9134,6 +9134,17 @@ ${shipName2 ? `Ship: ${shipName2}
9134
9134
 
9135
9135
  ${v.reason[0].toUpperCase()}${v.reason.slice(1)}. Set back to \`in_progress\`, still held by this ship; push with a plain \`git push --set-upstream ${v.remote} <branch>\` (never \`--force\`) and set \`fixed\` again.`;
9136
9136
  }
9137
+ async function takenOverSince(i, ticketRow) {
9138
+ if (!i.since || !i.writer.events) return false;
9139
+ const from = Date.parse(i.since);
9140
+ const evs = await i.writer.events("taken_over");
9141
+ return evs.some((e) => {
9142
+ if (e.record_id !== ticketRow) return false;
9143
+ if (Date.parse(e.occurred_at ?? e.created_at ?? "") < from) return false;
9144
+ const by = / by (.+?); /.exec(e.message ?? "")?.[1];
9145
+ return !!by && by !== i.shipName;
9146
+ });
9147
+ }
9137
9148
  async function enforceHandoff(i) {
9138
9149
  try {
9139
9150
  const t = await i.writer.ticket(i.ticketId);
@@ -9159,6 +9170,18 @@ async function enforceHandoff(i) {
9159
9170
  return { kind: "failed", why: v.reason };
9160
9171
  }
9161
9172
  const heldBy = i.hold ? t[i.hold.column] : void 0;
9173
+ if (i.hold && heldBy && heldBy !== i.hold.shipId) {
9174
+ i.log.warn(`${t.issue_id} is held by another ship now \u2014 this run's hand-off check is stale, left alone`, log);
9175
+ return { kind: "unverified", why: "ticket is held by another ship" };
9176
+ }
9177
+ if (await takenOverSince(i, t.id)) {
9178
+ i.log.warn(`${t.issue_id} was taken over by another ship during this run \u2014 left alone`, log);
9179
+ return { kind: "unverified", why: "ticket was taken over during this run" };
9180
+ }
9181
+ if (!v.local) {
9182
+ i.log.warn(`${t.issue_id} has no local branch on this ship, so it is not this ship's hand-off to bounce: ${v.reason}`, log);
9183
+ return { kind: "unverified", why: "no local branch on this ship" };
9184
+ }
9162
9185
  await i.writer.updateTicket(t.id, {
9163
9186
  status: i.contract.statuses.building,
9164
9187
  ...t.assignee_id ? {} : { assignee_id: i.seatId },
@@ -9198,6 +9221,7 @@ async function guardRunHandoff(route, ship, tracker, role, workingId, log, since
9198
9221
  seatId,
9199
9222
  shipName: ship.name,
9200
9223
  hold: affinity ? { column: affinity.column, shipId: affinity.shipId } : void 0,
9224
+ since: since2,
9201
9225
  log
9202
9226
  });
9203
9227
  if (outcome.kind === "pushed") await retireRunWorktree(tracker, target.dir, cfg, t, role, seatId, ship.name, log);
@@ -12158,17 +12182,15 @@ function schedulerInstalled(plan, exists, crontab) {
12158
12182
  }
12159
12183
 
12160
12184
  // src/worktree-sweep.ts
12161
- import { existsSync as existsSync19 } from "fs";
12162
- import { basename as basename8 } from "path";
12185
+ import { existsSync as existsSync19, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "fs";
12186
+ import { basename as basename8, dirname as dirname11, join as join19, resolve as resolve4 } from "path";
12163
12187
  function planWorktreeSweep(target, tickets, contract) {
12164
12188
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
12165
12189
  const verified = contract.statuses.verified;
12166
12190
  const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
12167
12191
  const actions = [];
12168
- for (const w of worktrees(target.dir)) {
12169
- const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
12170
- if (!m) continue;
12171
- const ticket = byNumber.get(m[1]);
12192
+ for (const w of worktrees(target.dir).slice(1)) {
12193
+ const ticket = ticketForWorktree(w, byNumber, contract);
12172
12194
  if (!ticket) continue;
12173
12195
  const isVerified = ticket.status === verified;
12174
12196
  if (!isVerified && !terminal.has(ticket.status)) continue;
@@ -12182,18 +12204,80 @@ function planWorktreeSweep(target, tickets, contract) {
12182
12204
  }
12183
12205
  return actions;
12184
12206
  }
12207
+ var DIR_NUMBER = /(?:^|-)(\d+)$/;
12208
+ var DIR_NUMBER_SUFFIXED = /(?:^|-)(\d+)(?:-[a-z0-9]{1,8})?$/;
12209
+ var BRANCH_STEM = /^(?:.*\/)?([^/]+?-\d+)(?:-.*)?$/;
12210
+ function ticketForWorktree(w, byNumber, contract) {
12211
+ if (w.branch) {
12212
+ const stem = BRANCH_STEM.exec(w.branch)?.[1];
12213
+ const num = stem && /-(\d+)$/.exec(stem)?.[1];
12214
+ const t = num ? byNumber.get(num) : void 0;
12215
+ if (t && ticketStems(t).has(stem)) return t;
12216
+ }
12217
+ const dir = basename8(w.path);
12218
+ const m = DIR_NUMBER.exec(dir);
12219
+ if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
12220
+ if (!w.branch) {
12221
+ const t = byNumber.get(DIR_NUMBER_SUFFIXED.exec(dir)?.[1] ?? "");
12222
+ if (t && t.status === contract.statuses.deployed) return t;
12223
+ }
12224
+ return void 0;
12225
+ }
12185
12226
  function worktreeTicketNumbers(target) {
12186
12227
  const nums = /* @__PURE__ */ new Set();
12187
- for (const w of worktrees(target.dir)) {
12188
- const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
12228
+ for (const w of worktrees(target.dir).slice(1)) {
12229
+ if (w.branch) {
12230
+ const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
12231
+ if (n) nums.add(n);
12232
+ }
12233
+ const m = DIR_NUMBER_SUFFIXED.exec(basename8(w.path));
12189
12234
  if (m) nums.add(m[1]);
12190
12235
  }
12236
+ for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
12191
12237
  return [...nums];
12192
12238
  }
12193
- async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin") {
12239
+ function siblingWorktreeDirs(target) {
12240
+ const parent = dirname11(target.dir.replace(/[/\\]+$/, ""));
12241
+ const stem = `${basename8(target.dir.replace(/[/\\]+$/, ""))}-`;
12242
+ const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
12243
+ let names;
12244
+ try {
12245
+ names = readdirSync5(parent);
12246
+ } catch {
12247
+ return [];
12248
+ }
12249
+ const out = [];
12250
+ for (const name of names) {
12251
+ if (!name.startsWith(stem)) continue;
12252
+ const m = DIR_NUMBER_SUFFIXED.exec(name);
12253
+ const path = join19(parent, name);
12254
+ if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
12255
+ }
12256
+ return out;
12257
+ }
12258
+ function orphanedWorktreeDirs(target, tickets, contract) {
12259
+ const deployed = new Set(
12260
+ tickets.filter((t) => t.status === contract.statuses.deployed).map((t) => t.issue_id.replace(/^\D+/, ""))
12261
+ );
12262
+ return siblingWorktreeDirs(target).filter((d) => deployed.has(d.number)).map((d) => d.path);
12263
+ }
12264
+ function warnOrphanedWorktreeDirs(paths, log) {
12265
+ for (const p of paths) {
12266
+ log.warn(`orphaned directory ${p}: its ticket is deployed but it is not a registered worktree; the sweep will not delete it \u2014 remove it by hand`, { step: "worktree" });
12267
+ }
12268
+ }
12269
+ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin", base) {
12194
12270
  let removed = 0;
12195
12271
  let keptBranches = 0;
12196
12272
  for (const a of actions) {
12273
+ if (!dryRun && base && !a.keepBranch && existsSync19(a.path) && isHandBackMerge(a.path, base)) {
12274
+ if (gitOk(a.path, ["merge", "--abort"]) !== null) {
12275
+ log.emit(
12276
+ `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
12277
+ { ticket: a.ticket.issue_id, step: "worktree" }
12278
+ );
12279
+ }
12280
+ }
12197
12281
  if (!dryRun && existsSync19(a.path)) {
12198
12282
  let dirty = false;
12199
12283
  try {
@@ -12233,7 +12317,12 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
12233
12317
  );
12234
12318
  continue;
12235
12319
  }
12236
- gitOk(cwd, ["worktree", "remove", "--force", a.path]);
12320
+ try {
12321
+ git(cwd, ["worktree", "remove", "--force", a.path]);
12322
+ } catch (e) {
12323
+ log.warn(`could not remove worktree ${a.path} for ${a.ticket.issue_id}: ${e.message}`, { ticket: a.ticket.issue_id, step: "worktree" });
12324
+ continue;
12325
+ }
12237
12326
  if (!a.keepBranch && a.branch) {
12238
12327
  gitOk(cwd, ["branch", "-D", a.branch]);
12239
12328
  if (remoteConfigured(cwd, remote) && !deleteRemoteBranch(cwd, remote, a.branch)) {
@@ -12246,8 +12335,22 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
12246
12335
  );
12247
12336
  removed++;
12248
12337
  }
12338
+ if (!dryRun && actions.length) gitOk(cwd, ["worktree", "prune"]);
12249
12339
  return { removed, keptBranches };
12250
12340
  }
12341
+ function isHandBackMerge(path, base) {
12342
+ if (gitOk(path, ["rev-parse", "-q", "--verify", "MERGE_HEAD"]) === null) return false;
12343
+ const msgPath = gitOk(path, ["rev-parse", "--git-path", "MERGE_MSG"]);
12344
+ if (!msgPath) return false;
12345
+ let first;
12346
+ try {
12347
+ first = readFileSync14(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
12348
+ } catch {
12349
+ return false;
12350
+ }
12351
+ const escaped = base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
12352
+ return new RegExp(`^Merge (?:remote-tracking )?branch '(?:[^']+/)?${escaped}'`).test(first);
12353
+ }
12251
12354
  function remoteBranchTicketNumbers(cwd, remote, base) {
12252
12355
  if (!remoteConfigured(cwd, remote)) return [];
12253
12356
  const prefix = `refs/remotes/${remote}/`;
@@ -12273,13 +12376,18 @@ function deployedBranchStems(tickets, contract) {
12273
12376
  const stems = /* @__PURE__ */ new Set();
12274
12377
  for (const t of tickets) {
12275
12378
  if (t.status !== contract.statuses.deployed) continue;
12276
- const num = t.issue_id.replace(/^\D+/, "");
12277
- const unpadded = String(Number.parseInt(num, 10));
12278
- const prefixes = ["issue", t.project_issue_prefix?.toLowerCase()].filter((p) => !!p);
12279
- for (const p of prefixes) {
12280
- stems.add(`${p}-${num}`);
12281
- stems.add(`${p}-${unpadded}`);
12282
- }
12379
+ for (const st of ticketStems(t)) stems.add(st);
12380
+ }
12381
+ return stems;
12382
+ }
12383
+ function ticketStems(t) {
12384
+ const stems = /* @__PURE__ */ new Set();
12385
+ const num = t.issue_id.replace(/^\D+/, "");
12386
+ const unpadded = String(Number.parseInt(num, 10));
12387
+ const prefixes = ["issue", t.project_issue_prefix?.toLowerCase()].filter((p) => !!p);
12388
+ for (const p of prefixes) {
12389
+ stems.add(`${p}-${num}`);
12390
+ stems.add(`${p}-${unpadded}`);
12283
12391
  }
12284
12392
  return stems;
12285
12393
  }
@@ -12290,7 +12398,8 @@ function isTicketBranch(name, base, stems) {
12290
12398
  }
12291
12399
  function sweepCandidatesExist(cwd, base) {
12292
12400
  const linked = worktrees(cwd).slice(1);
12293
- if (linked.some((w) => /(?:^|-)\d+$/.test(basename8(w.path)))) return true;
12401
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename8(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
12402
+ if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
12294
12403
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
12295
12404
  }
12296
12405
  function localBranchTicketNumbers(cwd, base) {
@@ -12340,14 +12449,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
12340
12449
  }
12341
12450
 
12342
12451
  // src/stream-sweep.ts
12343
- import { existsSync as existsSync20, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
12344
- import { join as join19 } from "path";
12452
+ import { existsSync as existsSync20, readdirSync as readdirSync6, rmSync as rmSync8, statSync as statSync7 } from "fs";
12453
+ import { join as join20 } from "path";
12345
12454
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
12346
12455
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
12347
- const dir = join19(stateDir, "streams");
12456
+ const dir = join20(stateDir, "streams");
12348
12457
  if (!existsSync20(dir)) return [];
12349
12458
  const cutoff = now - retentionDays * DAY_MS2;
12350
- return readdirSync5(dir).map((name) => join19(dir, name)).filter((path) => {
12459
+ return readdirSync6(dir).map((name) => join20(dir, name)).filter((path) => {
12351
12460
  try {
12352
12461
  return statSync7(path).mtimeMs < cutoff;
12353
12462
  } catch {
@@ -12375,7 +12484,7 @@ function applyStreamSweep(paths, dryRun, log) {
12375
12484
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
12376
12485
  if (!existsSync20(stateDir)) return [];
12377
12486
  const cutoff = now - retentionDays * DAY_MS2;
12378
- return readdirSync5(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join19(stateDir, name)).filter((path) => {
12487
+ return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join20(stateDir, name)).filter((path) => {
12379
12488
  try {
12380
12489
  return statSync7(path).mtimeMs < cutoff;
12381
12490
  } catch {
@@ -12385,24 +12494,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
12385
12494
  }
12386
12495
 
12387
12496
  // src/cli.ts
12388
- import { readFileSync as readFileSync19, existsSync as existsSync26, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
12497
+ import { readFileSync as readFileSync20, existsSync as existsSync26, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
12389
12498
  import { createInterface } from "readline/promises";
12390
12499
  import { dirname as dirOf, resolve as resolvePath } from "path";
12391
12500
 
12392
12501
  // src/ssh-keys.ts
12393
12502
  import { execFileSync as execFileSync3 } from "child_process";
12394
- import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
12395
- import { dirname as dirname11, join as join20 } from "path";
12503
+ import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync15, chmodSync } from "fs";
12504
+ import { dirname as dirname12, join as join21 } from "path";
12396
12505
  var SshKeyError = class extends Error {
12397
12506
  };
12398
12507
  function shipSshKeyPathFor(stateDir) {
12399
- return join20(stateDir, "ssh", "id_ed25519");
12508
+ return join21(stateDir, "ssh", "id_ed25519");
12400
12509
  }
12401
12510
  function ensureShipSshKeypair(stateDir) {
12402
12511
  const privateKeyPath = shipSshKeyPathFor(stateDir);
12403
12512
  const publicKeyPath = `${privateKeyPath}.pub`;
12404
12513
  if (!existsSync21(privateKeyPath)) {
12405
- mkdirSync9(dirname11(privateKeyPath), { recursive: true });
12514
+ mkdirSync9(dirname12(privateKeyPath), { recursive: true });
12406
12515
  try {
12407
12516
  execFileSync3("ssh-keygen", [
12408
12517
  "-t",
@@ -12425,7 +12534,7 @@ function ensureShipSshKeypair(stateDir) {
12425
12534
  if (!existsSync21(publicKeyPath)) {
12426
12535
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
12427
12536
  }
12428
- const publicKey = readFileSync14(publicKeyPath, "utf8").trim();
12537
+ const publicKey = readFileSync15(publicKeyPath, "utf8").trim();
12429
12538
  return { privateKeyPath, publicKeyPath, publicKey };
12430
12539
  }
12431
12540
  function sshKeygenAvailable() {
@@ -12641,9 +12750,9 @@ function syncAllPassengerCheckouts(cfg) {
12641
12750
 
12642
12751
  // src/tunnel.ts
12643
12752
  import { spawn as spawn3 } from "child_process";
12644
- import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync6 } from "fs";
12753
+ import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync16, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
12645
12754
  import { randomInt } from "crypto";
12646
- import { join as join21 } from "path";
12755
+ import { join as join22 } from "path";
12647
12756
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
12648
12757
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
12649
12758
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -12710,20 +12819,20 @@ function startTunnel(opts) {
12710
12819
  };
12711
12820
  }
12712
12821
  function tunnelStatePath(stateDir, workspaceId) {
12713
- return join21(stateDir, "passengers", `${workspaceId}.json`);
12822
+ return join22(stateDir, "passengers", `${workspaceId}.json`);
12714
12823
  }
12715
12824
  function readPersistedTunnel(stateDir, workspaceId) {
12716
12825
  const path = tunnelStatePath(stateDir, workspaceId);
12717
12826
  if (!existsSync22(path)) return void 0;
12718
12827
  try {
12719
- return JSON.parse(readFileSync15(path, "utf8"));
12828
+ return JSON.parse(readFileSync16(path, "utf8"));
12720
12829
  } catch {
12721
12830
  return void 0;
12722
12831
  }
12723
12832
  }
12724
12833
  function writePersistedTunnel(stateDir, state) {
12725
12834
  const path = tunnelStatePath(stateDir, state.workspaceId);
12726
- mkdirSync10(join21(stateDir, "passengers"), { recursive: true });
12835
+ mkdirSync10(join22(stateDir, "passengers"), { recursive: true });
12727
12836
  writeFileSync11(path, `${JSON.stringify(state, null, 2)}
12728
12837
  `);
12729
12838
  }
@@ -12821,9 +12930,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12821
12930
  }
12822
12931
  }
12823
12932
  function readAllPersistedWorkspaceIds(stateDir) {
12824
- const dir = join21(stateDir, "passengers");
12933
+ const dir = join22(stateDir, "passengers");
12825
12934
  if (!existsSync22(dir)) return [];
12826
- return readdirSync6(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
12935
+ return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
12827
12936
  }
12828
12937
 
12829
12938
  // src/mcp-endpoint-registry.ts
@@ -12943,8 +13052,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
12943
13052
  import { createServer as createHttpServer } from "http";
12944
13053
  import { spawn as spawn4 } from "child_process";
12945
13054
  import { randomBytes } from "crypto";
12946
- import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
12947
- import { join as join22 } from "path";
13055
+ import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
13056
+ import { join as join23 } from "path";
12948
13057
 
12949
13058
  // src/secret-compare.ts
12950
13059
  import { timingSafeEqual } from "crypto";
@@ -13033,20 +13142,20 @@ var defaultSpawn2 = (cmd, args, env) => {
13033
13142
  return child;
13034
13143
  };
13035
13144
  function syncDaemonStatePath(stateDir, workspaceId) {
13036
- return join22(stateDir, "passengers", `${workspaceId}-sync.json`);
13145
+ return join23(stateDir, "passengers", `${workspaceId}-sync.json`);
13037
13146
  }
13038
13147
  function readPersistedSyncDaemon(stateDir, workspaceId) {
13039
13148
  const path = syncDaemonStatePath(stateDir, workspaceId);
13040
13149
  if (!existsSync23(path)) return void 0;
13041
13150
  try {
13042
- return JSON.parse(readFileSync16(path, "utf8"));
13151
+ return JSON.parse(readFileSync17(path, "utf8"));
13043
13152
  } catch {
13044
13153
  return void 0;
13045
13154
  }
13046
13155
  }
13047
13156
  function writePersistedSyncDaemon(stateDir, state) {
13048
13157
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
13049
- mkdirSync11(join22(stateDir, "passengers"), { recursive: true });
13158
+ mkdirSync11(join23(stateDir, "passengers"), { recursive: true });
13050
13159
  writeFileSync12(path, `${JSON.stringify(state, null, 2)}
13051
13160
  `);
13052
13161
  }
@@ -13067,10 +13176,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
13067
13176
  }
13068
13177
  }
13069
13178
  function readAllPersistedSyncWorkspaceIds(stateDir) {
13070
- const dir = join22(stateDir, "passengers");
13179
+ const dir = join23(stateDir, "passengers");
13071
13180
  if (!existsSync23(dir)) return [];
13072
13181
  const suffix = "-sync.json";
13073
- return readdirSync7(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
13182
+ return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
13074
13183
  }
13075
13184
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
13076
13185
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -13125,12 +13234,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
13125
13234
  }
13126
13235
 
13127
13236
  // src/daemon.ts
13128
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
13129
- import { join as join24 } from "path";
13237
+ import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync19, readdirSync as readdirSync10, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
13238
+ import { join as join25 } from "path";
13130
13239
 
13131
13240
  // src/interrupted.ts
13132
- import { existsSync as existsSync24, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
13133
- import { join as join23 } from "path";
13241
+ import { existsSync as existsSync24, readdirSync as readdirSync9, readFileSync as readFileSync18 } from "fs";
13242
+ import { join as join24 } from "path";
13134
13243
  var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
13135
13244
  function interruptedComment(b) {
13136
13245
  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.";
@@ -13150,19 +13259,19 @@ function runFinished(streamPath) {
13150
13259
  const path = `${streamPath}.meta.json`;
13151
13260
  if (!existsSync24(path)) return false;
13152
13261
  try {
13153
- return JSON.parse(readFileSync17(path, "utf8")).running !== true;
13262
+ return JSON.parse(readFileSync18(path, "utf8")).running !== true;
13154
13263
  } catch {
13155
13264
  return true;
13156
13265
  }
13157
13266
  }
13158
13267
  function orphanedStreams(o) {
13159
- const dir = join23(o.stateDir, "streams");
13268
+ const dir = join24(o.stateDir, "streams");
13160
13269
  if (!existsSync24(dir)) return [];
13161
13270
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
13162
13271
  const out = [];
13163
- for (const name of readdirSync8(dir)) {
13272
+ for (const name of readdirSync9(dir)) {
13164
13273
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13165
- const streamPath = join23(dir, name);
13274
+ const streamPath = join24(dir, name);
13166
13275
  if (runFinished(streamPath)) continue;
13167
13276
  const sidecar = readRunSidecar(streamPath);
13168
13277
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -13215,7 +13324,7 @@ async function reconcileRuns(o) {
13215
13324
  // src/daemon.ts
13216
13325
  var DEFAULT_FLOOR_MS = 5e3;
13217
13326
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
13218
- var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
13327
+ var defaultSleep = (ms2) => new Promise((resolve6) => setTimeout(resolve6, ms2));
13219
13328
  async function runDaemonLoop(o) {
13220
13329
  const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
13221
13330
  const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
@@ -13261,8 +13370,8 @@ function latestMtime(path) {
13261
13370
  if (stat.isFile()) return stat.mtimeMs;
13262
13371
  if (!stat.isDirectory()) return 0;
13263
13372
  let latest = stat.mtimeMs;
13264
- for (const entry of readdirSync9(path, { withFileTypes: true })) {
13265
- latest = Math.max(latest, latestMtime(join24(path, entry.name)));
13373
+ for (const entry of readdirSync10(path, { withFileTypes: true })) {
13374
+ latest = Math.max(latest, latestMtime(join25(path, entry.name)));
13266
13375
  }
13267
13376
  return latest;
13268
13377
  }
@@ -13274,10 +13383,10 @@ function makeStaleChecker(paths, opts = {}) {
13274
13383
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync25(opts.reloadSentinel);
13275
13384
  }
13276
13385
  function reloadSentinelFor(stateDir) {
13277
- return join24(stateDir, "reload-requested");
13386
+ return join25(stateDir, "reload-requested");
13278
13387
  }
13279
13388
  function daemonStampFor(stateDir) {
13280
- return join24(stateDir, "daemon.started");
13389
+ return join25(stateDir, "daemon.started");
13281
13390
  }
13282
13391
  function noteDaemonStarted(stateDir, now = Date.now()) {
13283
13392
  mkdirSync12(stateDir, { recursive: true });
@@ -13287,7 +13396,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
13287
13396
  }
13288
13397
  function readDaemonStarted(stateDir) {
13289
13398
  try {
13290
- const v = JSON.parse(readFileSync18(daemonStampFor(stateDir), "utf8")).startedAt;
13399
+ const v = JSON.parse(readFileSync19(daemonStampFor(stateDir), "utf8")).startedAt;
13291
13400
  return typeof v === "number" ? v : null;
13292
13401
  } catch {
13293
13402
  return null;
@@ -13643,7 +13752,7 @@ async function buildEnvironment(route, ship, ticket) {
13643
13752
  }
13644
13753
 
13645
13754
  // src/cli.ts
13646
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname12(process.execPath) : resolve4(dirname12(fileURLToPath(import.meta.url)), "..");
13755
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname13(process.execPath) : resolve5(dirname13(fileURLToPath(import.meta.url)), "..");
13647
13756
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
13648
13757
  var CYCLE_SECONDS2 = 120;
13649
13758
  var STALE_CYCLES = 3;
@@ -14161,7 +14270,7 @@ if (command === "passenger-sync-daemon") {
14161
14270
  }
14162
14271
  if (host === "linux" && hasSystemd) {
14163
14272
  const label = labelFor(CREW_HOME, "run");
14164
- const staleTimer = join25(homedir8(), ".config", "systemd", "user", `${label}.timer`);
14273
+ const staleTimer = join26(homedir8(), ".config", "systemd", "user", `${label}.timer`);
14165
14274
  if (existsSync26(staleTimer)) {
14166
14275
  process.stdout.write(
14167
14276
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -14223,7 +14332,7 @@ if (command === "passenger-sync-daemon") {
14223
14332
  for (const a of plan.actions) {
14224
14333
  if (a.kind === "enable") {
14225
14334
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
14226
- writeFileSync14(cfg.configFile, planEnable(readFileSync19(cfg.configFile, "utf8"), a.route));
14335
+ writeFileSync14(cfg.configFile, planEnable(readFileSync20(cfg.configFile, "utf8"), a.route));
14227
14336
  enabledSound.add(a.route);
14228
14337
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
14229
14338
  } else {
@@ -14312,9 +14421,10 @@ ${changed2.map((c) => ` - ${c}
14312
14421
  ]);
14313
14422
  const scoped = ticketsByRepo(c, await tracker.ticketsByNumbers([...nums], truncated)).byRepo.get(target.name) ?? [];
14314
14423
  const actions = planWorktreeSweep(target, scoped, tracker.contract);
14424
+ warnOrphanedWorktreeDirs(orphanedWorktreeDirs(target, scoped, tracker.contract), remit);
14315
14425
  if (actions.length) {
14316
14426
  remit.enter("worktree");
14317
- const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote);
14427
+ const r = await applyWorktreeSweep(target.dir, actions, dryRun2, remit, branch.remote, branch.base);
14318
14428
  remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
14319
14429
  }
14320
14430
  const orphaned = planLocalBranchCleanup(target.dir, branch.base, scoped, tracker.contract);
@@ -15199,7 +15309,7 @@ ${changed2.map((c) => ` - ${c}
15199
15309
  process.exit(1);
15200
15310
  }
15201
15311
  requireArmed("run the daemon loop");
15202
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join25(CREW_HOME, "src"), join25(CREW_HOME, "dist"), join25(CREW_HOME, "bin")];
15312
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join26(CREW_HOME, "src"), join26(CREW_HOME, "dist"), join26(CREW_HOME, "bin")];
15203
15313
  const configPaths = daemonConfigPaths(cfg);
15204
15314
  watchPaths.push(...configPaths);
15205
15315
  noteDaemonStarted(cfg.ship.stateDir);
@@ -15374,7 +15484,7 @@ ${changed2.map((c) => ` - ${c}
15374
15484
  `);
15375
15485
  if (!dryRun) {
15376
15486
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
15377
- const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15487
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
15378
15488
  raw.agentPersonas = personaResult.agentPersonas;
15379
15489
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
15380
15490
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
@@ -15444,7 +15554,7 @@ ${changed2.map((c) => ` - ${c}
15444
15554
  `);
15445
15555
  if (!dryRun) {
15446
15556
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
15447
- const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15557
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
15448
15558
  raw.agentSkills = result.agentSkills;
15449
15559
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
15450
15560
  writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -15460,7 +15570,7 @@ ${changed2.map((c) => ` - ${c}
15460
15570
  process.exit(2);
15461
15571
  };
15462
15572
  if (sub !== "add" && sub !== "list") usage2();
15463
- const configText = readFileSync19(cfg.configFile, "utf8");
15573
+ const configText = readFileSync20(cfg.configFile, "utf8");
15464
15574
  let routeName = positional[2];
15465
15575
  let addPath = ".";
15466
15576
  let addName;
@@ -15484,13 +15594,13 @@ ${changed2.map((c) => ` - ${c}
15484
15594
  let resolvedRemotes;
15485
15595
  if (existsSync26(resolvedFile)) {
15486
15596
  try {
15487
- resolvedRemotes = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15597
+ resolvedRemotes = JSON.parse(readFileSync20(resolvedFile, "utf8"));
15488
15598
  } catch {
15489
15599
  }
15490
15600
  }
15491
15601
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
15492
15602
  const text = configText;
15493
- const base = dirname12(cfg.configFile);
15603
+ const base = dirname13(cfg.configFile);
15494
15604
  try {
15495
15605
  if (sub === "list") {
15496
15606
  const repos = configuredRepos(text, routeName, base);
@@ -15556,7 +15666,7 @@ ${changed2.map((c) => ` - ${c}
15556
15666
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
15557
15667
  `);
15558
15668
  if (existsSync26(resolvedFile)) {
15559
- const raw = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15669
+ const raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
15560
15670
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
15561
15671
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
15562
15672
  writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -15813,7 +15923,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15813
15923
  const hadPreviousFile = existsSync26(resolvedPath);
15814
15924
  if (hadPreviousFile) {
15815
15925
  try {
15816
- previous = JSON.parse(readFileSync19(resolvedPath, "utf8"));
15926
+ previous = JSON.parse(readFileSync20(resolvedPath, "utf8"));
15817
15927
  } catch {
15818
15928
  }
15819
15929
  }
@@ -15876,7 +15986,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15876
15986
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
15877
15987
  `);
15878
15988
  } else {
15879
- mkdirSync13(dirname12(resolvedPath), { recursive: true });
15989
+ mkdirSync13(dirname13(resolvedPath), { recursive: true });
15880
15990
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
15881
15991
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
15882
15992
  writeFileSync14(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -15904,7 +16014,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15904
16014
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
15905
16015
  `);
15906
16016
  } else {
15907
- mkdirSync13(dirname12(apiKeyFile), { recursive: true });
16017
+ mkdirSync13(dirname13(apiKeyFile), { recursive: true });
15908
16018
  writeFileSync14(apiKeyFile, `${apiKeyVar}=${apiKey}
15909
16019
  `, { mode: 384 });
15910
16020
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -15914,7 +16024,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15914
16024
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
15915
16025
  const wizard = firstRun;
15916
16026
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
15917
- const routeDir = value("dir") ?? (writeFirstRun ? join25(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
16027
+ const routeDir = value("dir") ?? (writeFirstRun ? join26(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
15918
16028
  const routeBlock = renderConnection(
15919
16029
  found,
15920
16030
  route2,
@@ -16106,6 +16216,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16106
16216
  await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
16107
16217
  ).byRepo.get(r.name) ?? [];
16108
16218
  const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
16219
+ warnOrphanedWorktreeDirs(orphanedWorktreeDirs(r, wtTickets, tracker.contract), emit);
16109
16220
  const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
16110
16221
  if (stale.length) {
16111
16222
  anything = true;
@@ -16113,7 +16224,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16113
16224
  }
16114
16225
  if (!actions.length) continue;
16115
16226
  anything = true;
16116
- const res = await applyWorktreeSweep(r.dir, actions, dryRun, emit, r.config.branch.remote);
16227
+ const res = await applyWorktreeSweep(r.dir, actions, dryRun, emit, r.config.branch.remote, r.config.branch.base);
16117
16228
  emit.emit(
16118
16229
  `swept ${res.removed} worktree(s) for ${r.name}, kept ${res.keptBranches} branch(es)`,
16119
16230
  { step: "worktree" }
@@ -16506,7 +16617,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16506
16617
  }
16507
16618
  case "log": {
16508
16619
  try {
16509
- const text = readFileSync19(cfg.ship.logFile, "utf8").split("\n");
16620
+ const text = readFileSync20(cfg.ship.logFile, "utf8").split("\n");
16510
16621
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
16511
16622
  `);
16512
16623
  } catch {
@@ -16727,7 +16838,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16727
16838
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
16728
16839
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
16729
16840
  try {
16730
- return readFileSync19(p, "utf8");
16841
+ return readFileSync20(p, "utf8");
16731
16842
  } catch {
16732
16843
  return null;
16733
16844
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.53.1",
3
+ "version": "0.53.3",
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",