@tablation/crew 0.53.2 → 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 +165 -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" });
@@ -12182,17 +12182,15 @@ function schedulerInstalled(plan, exists, crontab) {
12182
12182
  }
12183
12183
 
12184
12184
  // src/worktree-sweep.ts
12185
- import { existsSync as existsSync19 } from "fs";
12186
- 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";
12187
12187
  function planWorktreeSweep(target, tickets, contract) {
12188
12188
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
12189
12189
  const verified = contract.statuses.verified;
12190
12190
  const byNumber = new Map(tickets.map((t) => [t.issue_id.replace(/^\D+/, ""), t]));
12191
12191
  const actions = [];
12192
- for (const w of worktrees(target.dir)) {
12193
- const m = /(?:^|-)(\d+)$/.exec(basename8(w.path));
12194
- if (!m) continue;
12195
- const ticket = byNumber.get(m[1]);
12192
+ for (const w of worktrees(target.dir).slice(1)) {
12193
+ const ticket = ticketForWorktree(w, byNumber, contract);
12196
12194
  if (!ticket) continue;
12197
12195
  const isVerified = ticket.status === verified;
12198
12196
  if (!isVerified && !terminal.has(ticket.status)) continue;
@@ -12206,18 +12204,80 @@ function planWorktreeSweep(target, tickets, contract) {
12206
12204
  }
12207
12205
  return actions;
12208
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
+ }
12209
12226
  function worktreeTicketNumbers(target) {
12210
12227
  const nums = /* @__PURE__ */ new Set();
12211
- for (const w of worktrees(target.dir)) {
12212
- 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));
12213
12234
  if (m) nums.add(m[1]);
12214
12235
  }
12236
+ for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
12215
12237
  return [...nums];
12216
12238
  }
12217
- 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) {
12218
12270
  let removed = 0;
12219
12271
  let keptBranches = 0;
12220
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
+ }
12221
12281
  if (!dryRun && existsSync19(a.path)) {
12222
12282
  let dirty = false;
12223
12283
  try {
@@ -12257,7 +12317,12 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
12257
12317
  );
12258
12318
  continue;
12259
12319
  }
12260
- 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
+ }
12261
12326
  if (!a.keepBranch && a.branch) {
12262
12327
  gitOk(cwd, ["branch", "-D", a.branch]);
12263
12328
  if (remoteConfigured(cwd, remote) && !deleteRemoteBranch(cwd, remote, a.branch)) {
@@ -12270,8 +12335,22 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
12270
12335
  );
12271
12336
  removed++;
12272
12337
  }
12338
+ if (!dryRun && actions.length) gitOk(cwd, ["worktree", "prune"]);
12273
12339
  return { removed, keptBranches };
12274
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
+ }
12275
12354
  function remoteBranchTicketNumbers(cwd, remote, base) {
12276
12355
  if (!remoteConfigured(cwd, remote)) return [];
12277
12356
  const prefix = `refs/remotes/${remote}/`;
@@ -12297,13 +12376,18 @@ function deployedBranchStems(tickets, contract) {
12297
12376
  const stems = /* @__PURE__ */ new Set();
12298
12377
  for (const t of tickets) {
12299
12378
  if (t.status !== contract.statuses.deployed) continue;
12300
- const num = t.issue_id.replace(/^\D+/, "");
12301
- const unpadded = String(Number.parseInt(num, 10));
12302
- const prefixes = ["issue", t.project_issue_prefix?.toLowerCase()].filter((p) => !!p);
12303
- for (const p of prefixes) {
12304
- stems.add(`${p}-${num}`);
12305
- stems.add(`${p}-${unpadded}`);
12306
- }
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}`);
12307
12391
  }
12308
12392
  return stems;
12309
12393
  }
@@ -12314,7 +12398,8 @@ function isTicketBranch(name, base, stems) {
12314
12398
  }
12315
12399
  function sweepCandidatesExist(cwd, base) {
12316
12400
  const linked = worktrees(cwd).slice(1);
12317
- 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;
12318
12403
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
12319
12404
  }
12320
12405
  function localBranchTicketNumbers(cwd, base) {
@@ -12364,14 +12449,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
12364
12449
  }
12365
12450
 
12366
12451
  // src/stream-sweep.ts
12367
- import { existsSync as existsSync20, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
12368
- 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";
12369
12454
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
12370
12455
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
12371
- const dir = join19(stateDir, "streams");
12456
+ const dir = join20(stateDir, "streams");
12372
12457
  if (!existsSync20(dir)) return [];
12373
12458
  const cutoff = now - retentionDays * DAY_MS2;
12374
- return readdirSync5(dir).map((name) => join19(dir, name)).filter((path) => {
12459
+ return readdirSync6(dir).map((name) => join20(dir, name)).filter((path) => {
12375
12460
  try {
12376
12461
  return statSync7(path).mtimeMs < cutoff;
12377
12462
  } catch {
@@ -12399,7 +12484,7 @@ function applyStreamSweep(paths, dryRun, log) {
12399
12484
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
12400
12485
  if (!existsSync20(stateDir)) return [];
12401
12486
  const cutoff = now - retentionDays * DAY_MS2;
12402
- 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) => {
12403
12488
  try {
12404
12489
  return statSync7(path).mtimeMs < cutoff;
12405
12490
  } catch {
@@ -12409,24 +12494,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
12409
12494
  }
12410
12495
 
12411
12496
  // src/cli.ts
12412
- 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";
12413
12498
  import { createInterface } from "readline/promises";
12414
12499
  import { dirname as dirOf, resolve as resolvePath } from "path";
12415
12500
 
12416
12501
  // src/ssh-keys.ts
12417
12502
  import { execFileSync as execFileSync3 } from "child_process";
12418
- import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
12419
- 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";
12420
12505
  var SshKeyError = class extends Error {
12421
12506
  };
12422
12507
  function shipSshKeyPathFor(stateDir) {
12423
- return join20(stateDir, "ssh", "id_ed25519");
12508
+ return join21(stateDir, "ssh", "id_ed25519");
12424
12509
  }
12425
12510
  function ensureShipSshKeypair(stateDir) {
12426
12511
  const privateKeyPath = shipSshKeyPathFor(stateDir);
12427
12512
  const publicKeyPath = `${privateKeyPath}.pub`;
12428
12513
  if (!existsSync21(privateKeyPath)) {
12429
- mkdirSync9(dirname11(privateKeyPath), { recursive: true });
12514
+ mkdirSync9(dirname12(privateKeyPath), { recursive: true });
12430
12515
  try {
12431
12516
  execFileSync3("ssh-keygen", [
12432
12517
  "-t",
@@ -12449,7 +12534,7 @@ function ensureShipSshKeypair(stateDir) {
12449
12534
  if (!existsSync21(publicKeyPath)) {
12450
12535
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
12451
12536
  }
12452
- const publicKey = readFileSync14(publicKeyPath, "utf8").trim();
12537
+ const publicKey = readFileSync15(publicKeyPath, "utf8").trim();
12453
12538
  return { privateKeyPath, publicKeyPath, publicKey };
12454
12539
  }
12455
12540
  function sshKeygenAvailable() {
@@ -12665,9 +12750,9 @@ function syncAllPassengerCheckouts(cfg) {
12665
12750
 
12666
12751
  // src/tunnel.ts
12667
12752
  import { spawn as spawn3 } from "child_process";
12668
- 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";
12669
12754
  import { randomInt } from "crypto";
12670
- import { join as join21 } from "path";
12755
+ import { join as join22 } from "path";
12671
12756
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
12672
12757
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
12673
12758
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -12734,20 +12819,20 @@ function startTunnel(opts) {
12734
12819
  };
12735
12820
  }
12736
12821
  function tunnelStatePath(stateDir, workspaceId) {
12737
- return join21(stateDir, "passengers", `${workspaceId}.json`);
12822
+ return join22(stateDir, "passengers", `${workspaceId}.json`);
12738
12823
  }
12739
12824
  function readPersistedTunnel(stateDir, workspaceId) {
12740
12825
  const path = tunnelStatePath(stateDir, workspaceId);
12741
12826
  if (!existsSync22(path)) return void 0;
12742
12827
  try {
12743
- return JSON.parse(readFileSync15(path, "utf8"));
12828
+ return JSON.parse(readFileSync16(path, "utf8"));
12744
12829
  } catch {
12745
12830
  return void 0;
12746
12831
  }
12747
12832
  }
12748
12833
  function writePersistedTunnel(stateDir, state) {
12749
12834
  const path = tunnelStatePath(stateDir, state.workspaceId);
12750
- mkdirSync10(join21(stateDir, "passengers"), { recursive: true });
12835
+ mkdirSync10(join22(stateDir, "passengers"), { recursive: true });
12751
12836
  writeFileSync11(path, `${JSON.stringify(state, null, 2)}
12752
12837
  `);
12753
12838
  }
@@ -12845,9 +12930,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12845
12930
  }
12846
12931
  }
12847
12932
  function readAllPersistedWorkspaceIds(stateDir) {
12848
- const dir = join21(stateDir, "passengers");
12933
+ const dir = join22(stateDir, "passengers");
12849
12934
  if (!existsSync22(dir)) return [];
12850
- 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));
12851
12936
  }
12852
12937
 
12853
12938
  // src/mcp-endpoint-registry.ts
@@ -12967,8 +13052,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
12967
13052
  import { createServer as createHttpServer } from "http";
12968
13053
  import { spawn as spawn4 } from "child_process";
12969
13054
  import { randomBytes } from "crypto";
12970
- import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
12971
- 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";
12972
13057
 
12973
13058
  // src/secret-compare.ts
12974
13059
  import { timingSafeEqual } from "crypto";
@@ -13057,20 +13142,20 @@ var defaultSpawn2 = (cmd, args, env) => {
13057
13142
  return child;
13058
13143
  };
13059
13144
  function syncDaemonStatePath(stateDir, workspaceId) {
13060
- return join22(stateDir, "passengers", `${workspaceId}-sync.json`);
13145
+ return join23(stateDir, "passengers", `${workspaceId}-sync.json`);
13061
13146
  }
13062
13147
  function readPersistedSyncDaemon(stateDir, workspaceId) {
13063
13148
  const path = syncDaemonStatePath(stateDir, workspaceId);
13064
13149
  if (!existsSync23(path)) return void 0;
13065
13150
  try {
13066
- return JSON.parse(readFileSync16(path, "utf8"));
13151
+ return JSON.parse(readFileSync17(path, "utf8"));
13067
13152
  } catch {
13068
13153
  return void 0;
13069
13154
  }
13070
13155
  }
13071
13156
  function writePersistedSyncDaemon(stateDir, state) {
13072
13157
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
13073
- mkdirSync11(join22(stateDir, "passengers"), { recursive: true });
13158
+ mkdirSync11(join23(stateDir, "passengers"), { recursive: true });
13074
13159
  writeFileSync12(path, `${JSON.stringify(state, null, 2)}
13075
13160
  `);
13076
13161
  }
@@ -13091,10 +13176,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
13091
13176
  }
13092
13177
  }
13093
13178
  function readAllPersistedSyncWorkspaceIds(stateDir) {
13094
- const dir = join22(stateDir, "passengers");
13179
+ const dir = join23(stateDir, "passengers");
13095
13180
  if (!existsSync23(dir)) return [];
13096
13181
  const suffix = "-sync.json";
13097
- 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));
13098
13183
  }
13099
13184
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
13100
13185
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -13149,12 +13234,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
13149
13234
  }
13150
13235
 
13151
13236
  // src/daemon.ts
13152
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
13153
- 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";
13154
13239
 
13155
13240
  // src/interrupted.ts
13156
- import { existsSync as existsSync24, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
13157
- 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";
13158
13243
  var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
13159
13244
  function interruptedComment(b) {
13160
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.";
@@ -13174,19 +13259,19 @@ function runFinished(streamPath) {
13174
13259
  const path = `${streamPath}.meta.json`;
13175
13260
  if (!existsSync24(path)) return false;
13176
13261
  try {
13177
- return JSON.parse(readFileSync17(path, "utf8")).running !== true;
13262
+ return JSON.parse(readFileSync18(path, "utf8")).running !== true;
13178
13263
  } catch {
13179
13264
  return true;
13180
13265
  }
13181
13266
  }
13182
13267
  function orphanedStreams(o) {
13183
- const dir = join23(o.stateDir, "streams");
13268
+ const dir = join24(o.stateDir, "streams");
13184
13269
  if (!existsSync24(dir)) return [];
13185
13270
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
13186
13271
  const out = [];
13187
- for (const name of readdirSync8(dir)) {
13272
+ for (const name of readdirSync9(dir)) {
13188
13273
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13189
- const streamPath = join23(dir, name);
13274
+ const streamPath = join24(dir, name);
13190
13275
  if (runFinished(streamPath)) continue;
13191
13276
  const sidecar = readRunSidecar(streamPath);
13192
13277
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -13239,7 +13324,7 @@ async function reconcileRuns(o) {
13239
13324
  // src/daemon.ts
13240
13325
  var DEFAULT_FLOOR_MS = 5e3;
13241
13326
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
13242
- var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
13327
+ var defaultSleep = (ms2) => new Promise((resolve6) => setTimeout(resolve6, ms2));
13243
13328
  async function runDaemonLoop(o) {
13244
13329
  const floor = o.floorMs ?? DEFAULT_FLOOR_MS;
13245
13330
  const ceiling = o.ceilingMs ?? DEFAULT_CEILING_MS;
@@ -13285,8 +13370,8 @@ function latestMtime(path) {
13285
13370
  if (stat.isFile()) return stat.mtimeMs;
13286
13371
  if (!stat.isDirectory()) return 0;
13287
13372
  let latest = stat.mtimeMs;
13288
- for (const entry of readdirSync9(path, { withFileTypes: true })) {
13289
- 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)));
13290
13375
  }
13291
13376
  return latest;
13292
13377
  }
@@ -13298,10 +13383,10 @@ function makeStaleChecker(paths, opts = {}) {
13298
13383
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync25(opts.reloadSentinel);
13299
13384
  }
13300
13385
  function reloadSentinelFor(stateDir) {
13301
- return join24(stateDir, "reload-requested");
13386
+ return join25(stateDir, "reload-requested");
13302
13387
  }
13303
13388
  function daemonStampFor(stateDir) {
13304
- return join24(stateDir, "daemon.started");
13389
+ return join25(stateDir, "daemon.started");
13305
13390
  }
13306
13391
  function noteDaemonStarted(stateDir, now = Date.now()) {
13307
13392
  mkdirSync12(stateDir, { recursive: true });
@@ -13311,7 +13396,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
13311
13396
  }
13312
13397
  function readDaemonStarted(stateDir) {
13313
13398
  try {
13314
- const v = JSON.parse(readFileSync18(daemonStampFor(stateDir), "utf8")).startedAt;
13399
+ const v = JSON.parse(readFileSync19(daemonStampFor(stateDir), "utf8")).startedAt;
13315
13400
  return typeof v === "number" ? v : null;
13316
13401
  } catch {
13317
13402
  return null;
@@ -13667,7 +13752,7 @@ async function buildEnvironment(route, ship, ticket) {
13667
13752
  }
13668
13753
 
13669
13754
  // src/cli.ts
13670
- 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)), "..");
13671
13756
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
13672
13757
  var CYCLE_SECONDS2 = 120;
13673
13758
  var STALE_CYCLES = 3;
@@ -14185,7 +14270,7 @@ if (command === "passenger-sync-daemon") {
14185
14270
  }
14186
14271
  if (host === "linux" && hasSystemd) {
14187
14272
  const label = labelFor(CREW_HOME, "run");
14188
- const staleTimer = join25(homedir8(), ".config", "systemd", "user", `${label}.timer`);
14273
+ const staleTimer = join26(homedir8(), ".config", "systemd", "user", `${label}.timer`);
14189
14274
  if (existsSync26(staleTimer)) {
14190
14275
  process.stdout.write(
14191
14276
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -14247,7 +14332,7 @@ if (command === "passenger-sync-daemon") {
14247
14332
  for (const a of plan.actions) {
14248
14333
  if (a.kind === "enable") {
14249
14334
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
14250
- writeFileSync14(cfg.configFile, planEnable(readFileSync19(cfg.configFile, "utf8"), a.route));
14335
+ writeFileSync14(cfg.configFile, planEnable(readFileSync20(cfg.configFile, "utf8"), a.route));
14251
14336
  enabledSound.add(a.route);
14252
14337
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
14253
14338
  } else {
@@ -14336,9 +14421,10 @@ ${changed2.map((c) => ` - ${c}
14336
14421
  ]);
14337
14422
  const scoped = ticketsByRepo(c, await tracker.ticketsByNumbers([...nums], truncated)).byRepo.get(target.name) ?? [];
14338
14423
  const actions = planWorktreeSweep(target, scoped, tracker.contract);
14424
+ warnOrphanedWorktreeDirs(orphanedWorktreeDirs(target, scoped, tracker.contract), remit);
14339
14425
  if (actions.length) {
14340
14426
  remit.enter("worktree");
14341
- 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);
14342
14428
  remit.emit(`swept ${r.removed} worktree(s), kept ${r.keptBranches} branch(es)`, { step: "worktree" });
14343
14429
  }
14344
14430
  const orphaned = planLocalBranchCleanup(target.dir, branch.base, scoped, tracker.contract);
@@ -15223,7 +15309,7 @@ ${changed2.map((c) => ` - ${c}
15223
15309
  process.exit(1);
15224
15310
  }
15225
15311
  requireArmed("run the daemon loop");
15226
- 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")];
15227
15313
  const configPaths = daemonConfigPaths(cfg);
15228
15314
  watchPaths.push(...configPaths);
15229
15315
  noteDaemonStarted(cfg.ship.stateDir);
@@ -15398,7 +15484,7 @@ ${changed2.map((c) => ` - ${c}
15398
15484
  `);
15399
15485
  if (!dryRun) {
15400
15486
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
15401
- const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15487
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
15402
15488
  raw.agentPersonas = personaResult.agentPersonas;
15403
15489
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
15404
15490
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
@@ -15468,7 +15554,7 @@ ${changed2.map((c) => ` - ${c}
15468
15554
  `);
15469
15555
  if (!dryRun) {
15470
15556
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
15471
- const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15557
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync20(resolvedPath, "utf8")) : {};
15472
15558
  raw.agentSkills = result.agentSkills;
15473
15559
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
15474
15560
  writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -15484,7 +15570,7 @@ ${changed2.map((c) => ` - ${c}
15484
15570
  process.exit(2);
15485
15571
  };
15486
15572
  if (sub !== "add" && sub !== "list") usage2();
15487
- const configText = readFileSync19(cfg.configFile, "utf8");
15573
+ const configText = readFileSync20(cfg.configFile, "utf8");
15488
15574
  let routeName = positional[2];
15489
15575
  let addPath = ".";
15490
15576
  let addName;
@@ -15508,13 +15594,13 @@ ${changed2.map((c) => ` - ${c}
15508
15594
  let resolvedRemotes;
15509
15595
  if (existsSync26(resolvedFile)) {
15510
15596
  try {
15511
- resolvedRemotes = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15597
+ resolvedRemotes = JSON.parse(readFileSync20(resolvedFile, "utf8"));
15512
15598
  } catch {
15513
15599
  }
15514
15600
  }
15515
15601
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
15516
15602
  const text = configText;
15517
- const base = dirname12(cfg.configFile);
15603
+ const base = dirname13(cfg.configFile);
15518
15604
  try {
15519
15605
  if (sub === "list") {
15520
15606
  const repos = configuredRepos(text, routeName, base);
@@ -15580,7 +15666,7 @@ ${changed2.map((c) => ` - ${c}
15580
15666
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
15581
15667
  `);
15582
15668
  if (existsSync26(resolvedFile)) {
15583
- const raw = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15669
+ const raw = JSON.parse(readFileSync20(resolvedFile, "utf8"));
15584
15670
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
15585
15671
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
15586
15672
  writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -15837,7 +15923,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15837
15923
  const hadPreviousFile = existsSync26(resolvedPath);
15838
15924
  if (hadPreviousFile) {
15839
15925
  try {
15840
- previous = JSON.parse(readFileSync19(resolvedPath, "utf8"));
15926
+ previous = JSON.parse(readFileSync20(resolvedPath, "utf8"));
15841
15927
  } catch {
15842
15928
  }
15843
15929
  }
@@ -15900,7 +15986,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15900
15986
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
15901
15987
  `);
15902
15988
  } else {
15903
- mkdirSync13(dirname12(resolvedPath), { recursive: true });
15989
+ mkdirSync13(dirname13(resolvedPath), { recursive: true });
15904
15990
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
15905
15991
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
15906
15992
  writeFileSync14(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -15928,7 +16014,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15928
16014
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
15929
16015
  `);
15930
16016
  } else {
15931
- mkdirSync13(dirname12(apiKeyFile), { recursive: true });
16017
+ mkdirSync13(dirname13(apiKeyFile), { recursive: true });
15932
16018
  writeFileSync14(apiKeyFile, `${apiKeyVar}=${apiKey}
15933
16019
  `, { mode: 384 });
15934
16020
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -15938,7 +16024,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15938
16024
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
15939
16025
  const wizard = firstRun;
15940
16026
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
15941
- 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);
15942
16028
  const routeBlock = renderConnection(
15943
16029
  found,
15944
16030
  route2,
@@ -16130,6 +16216,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16130
16216
  await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
16131
16217
  ).byRepo.get(r.name) ?? [];
16132
16218
  const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
16219
+ warnOrphanedWorktreeDirs(orphanedWorktreeDirs(r, wtTickets, tracker.contract), emit);
16133
16220
  const stale = planRemoteBranchCleanup(r.dir, r.config.branch.remote, r.config.branch.base, brTickets, tracker.contract);
16134
16221
  if (stale.length) {
16135
16222
  anything = true;
@@ -16137,7 +16224,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16137
16224
  }
16138
16225
  if (!actions.length) continue;
16139
16226
  anything = true;
16140
- 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);
16141
16228
  emit.emit(
16142
16229
  `swept ${res.removed} worktree(s) for ${r.name}, kept ${res.keptBranches} branch(es)`,
16143
16230
  { step: "worktree" }
@@ -16530,7 +16617,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16530
16617
  }
16531
16618
  case "log": {
16532
16619
  try {
16533
- const text = readFileSync19(cfg.ship.logFile, "utf8").split("\n");
16620
+ const text = readFileSync20(cfg.ship.logFile, "utf8").split("\n");
16534
16621
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
16535
16622
  `);
16536
16623
  } catch {
@@ -16751,7 +16838,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16751
16838
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
16752
16839
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
16753
16840
  try {
16754
- return readFileSync19(p, "utf8");
16841
+ return readFileSync20(p, "utf8");
16755
16842
  } catch {
16756
16843
  return null;
16757
16844
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.53.2",
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",