@tablation/crew 0.47.1 → 0.47.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 +159 -80
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { resolve as resolve4, dirname as dirname12, join as join23, basename as basename9 } from "path";
4
+ import { resolve as resolve4, dirname as dirname12, join as join24, basename as basename9 } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { hostname as hostname2, homedir as homedir8 } from "os";
7
7
  import { spawnSync as spawnSync5 } from "child_process";
@@ -4030,9 +4030,11 @@ import {
4030
4030
  statSync as statSync4,
4031
4031
  unlinkSync as unlinkSync2,
4032
4032
  readFileSync as readFileSync9,
4033
- writeFileSync as writeFileSync6
4033
+ writeFileSync as writeFileSync6,
4034
+ readdirSync as readdirSync3,
4035
+ rmSync as rmSync6
4034
4036
  } from "fs";
4035
- import { dirname as dirname5, basename as basename4 } from "path";
4037
+ import { dirname as dirname5, basename as basename4, join as join10 } from "path";
4036
4038
  var GitError = class extends Error {
4037
4039
  };
4038
4040
  function streamText(x) {
@@ -4115,15 +4117,36 @@ function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-
4115
4117
  }
4116
4118
  return null;
4117
4119
  }
4118
- function materializeRemoteBranch(cwd, remote, branch) {
4119
- if (!remoteBranchExists(cwd, remote, branch)) {
4120
- return gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]) !== null;
4120
+ function levelRemoteBranch(cwd, remote, branch) {
4121
+ const local = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]);
4122
+ if (!remoteBranchExists(cwd, remote, branch)) return { exists: local !== null };
4123
+ if (local === null) {
4124
+ return { exists: gitOk(cwd, ["branch", "--track", branch, `${remote}/${branch}`]) !== null };
4125
+ }
4126
+ const remoteTip2 = gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/remotes/${remote}/${branch}`]);
4127
+ if (!remoteTip2 || remoteTip2 === local) return { exists: true };
4128
+ if (!isAncestor(cwd, local, remoteTip2)) return { exists: true };
4129
+ const holder = worktrees(cwd).find((w) => w.branch === branch && !w.prunable);
4130
+ if (holder) {
4131
+ if (gitOk(holder.path, ["status", "--porcelain"])) {
4132
+ return {
4133
+ exists: true,
4134
+ problem: `${branch} is behind ${remote}/${branch} and the worktree ${holder.path} that holds it has uncommitted or mid-merge changes`
4135
+ };
4136
+ }
4137
+ try {
4138
+ git(holder.path, ["merge", "--ff-only", "--quiet", `${remote}/${branch}`]);
4139
+ } catch (e) {
4140
+ return { exists: true, problem: `could not fast-forward ${branch} in ${holder.path}: ${e.message}` };
4141
+ }
4142
+ return { exists: true };
4121
4143
  }
4122
- if (gitOk(cwd, ["rev-parse", "-q", "--verify", `refs/heads/${branch}`]) === null) {
4123
- return gitOk(cwd, ["branch", "--track", branch, `${remote}/${branch}`]) !== null;
4144
+ try {
4145
+ git(cwd, ["fetch", "--quiet", remote, `${branch}:${branch}`]);
4146
+ } catch (e) {
4147
+ return { exists: true, problem: `could not move ${branch} to ${remote}/${branch}: ${e.message}` };
4124
4148
  }
4125
- gitOk(cwd, ["fetch", "--quiet", remote, `${branch}:${branch}`]);
4126
- return true;
4149
+ return { exists: true };
4127
4150
  }
4128
4151
  function deleteRemoteBranch(cwd, remote, branch) {
4129
4152
  if (!remoteBranchExists(cwd, remote, branch)) return true;
@@ -4367,12 +4390,36 @@ function recoverStrandedReleaseCheckouts(cwd) {
4367
4390
  if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
4368
4391
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
4369
4392
  gitOk(cwd, ["worktree", "remove", "--force", path]);
4393
+ const root = dirname5(path);
4394
+ if (basename4(root).startsWith("crew-release-")) rmSync6(root, { recursive: true, force: true });
4370
4395
  }
4371
4396
  }
4372
4397
  const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
4373
4398
  for (const b of stranded) gitOk(cwd, ["branch", "-q", "-D", b]);
4374
4399
  return stranded;
4375
4400
  }
4401
+ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3) {
4402
+ const removed = [];
4403
+ let names;
4404
+ try {
4405
+ names = readdirSync3(tmp);
4406
+ } catch {
4407
+ return removed;
4408
+ }
4409
+ for (const name of names) {
4410
+ if (!name.startsWith("crew-release-")) continue;
4411
+ const root = join10(tmp, name);
4412
+ try {
4413
+ const st = statSync4(root);
4414
+ if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
4415
+ if (existsSync9(join10(root, "checkout"))) continue;
4416
+ rmSync6(root, { recursive: true, force: true });
4417
+ removed.push(name);
4418
+ } catch {
4419
+ }
4420
+ }
4421
+ return removed;
4422
+ }
4376
4423
  function pushBranch(cwd, remote, branch, dst = branch) {
4377
4424
  git(cwd, ["push", remote, `${branch}:${dst}`]);
4378
4425
  }
@@ -6259,16 +6306,17 @@ function locateBranchForTicket(dir, cfg, t, role, fetchedDirs) {
6259
6306
  const found = remoteBranchForIssue(dir, remote, t.issue_id, branchTemplates(cfg, t), branchRenderer(t, role));
6260
6307
  return found ? { branch: found, where: "remote" } : null;
6261
6308
  }
6262
- function materializeBranchForTicket(dir, cfg, t, role) {
6309
+ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
6263
6310
  const loc = locateBranchForTicket(dir, cfg, t, role);
6264
6311
  if (!loc) return null;
6265
- materializeRemoteBranch(dir, cfg.branch.remote, loc.branch);
6312
+ const r = levelRemoteBranch(dir, cfg.branch.remote, loc.branch);
6313
+ if (r.problem) onProblem?.(r.problem);
6266
6314
  return loc.branch;
6267
6315
  }
6268
6316
 
6269
6317
  // src/poll.ts
6270
6318
  import { writeFileSync as writeFileSync7 } from "fs";
6271
- import { join as join10 } from "path";
6319
+ import { join as join11 } from "path";
6272
6320
 
6273
6321
  // src/stalled.ts
6274
6322
  var STALLED_AFTER_CYCLES = 30;
@@ -6545,7 +6593,7 @@ async function decideCycle(o) {
6545
6593
  }
6546
6594
  return decision;
6547
6595
  }
6548
- var digestPath = (stateDir, route, role) => join10(stateDir, `digest-${route}-${role}.md`);
6596
+ var digestPath = (stateDir, route, role) => join11(stateDir, `digest-${route}-${role}.md`);
6549
6597
  function writeDigest(o, d, role, stateDir) {
6550
6598
  const me = o.route.resolved?.seats[role];
6551
6599
  if (!me) return false;
@@ -6720,6 +6768,11 @@ function planConflictBounce(cwd, ticket, branch, base, paths, events) {
6720
6768
  function refreshBranch(worktree, base) {
6721
6769
  const dirty = gitOk(worktree, ["status", "--porcelain"]);
6722
6770
  if (dirty) return { clean: false, paths: [], why: "the worktree has uncommitted changes" };
6771
+ const upstream = gitOk(worktree, ["rev-parse", "-q", "--verify", "@{upstream}"]);
6772
+ const head = gitOk(worktree, ["rev-parse", "HEAD"]);
6773
+ if (upstream && head && upstream !== head && isAncestor(worktree, head, upstream)) {
6774
+ return { clean: false, paths: [], behind: true, why: "the branch is behind its remote; fast-forward it before merging the base in" };
6775
+ }
6723
6776
  try {
6724
6777
  git(worktree, ["merge", "--no-edit", base]);
6725
6778
  return { clean: true, paths: [] };
@@ -6787,6 +6840,10 @@ async function applyConflictBounce(writer, b, contract, memberId, log, dryRun) {
6787
6840
  log.emit(`merged ${b.base} into ${b.branch} cleanly \u2014 it was stale, not conflicting`, t);
6788
6841
  return { kind: "refreshed" };
6789
6842
  }
6843
+ if (r.behind) {
6844
+ log.warn(`not handing ${b.ticket.issue_id} back: ${r.why}`, t);
6845
+ return { kind: "failed", why: r.why ?? "branch behind its remote" };
6846
+ }
6790
6847
  merged = r.paths.length > 0;
6791
6848
  if (r.why) log.warn(`could not merge ${b.base} into ${b.branch}: ${r.why}`, t);
6792
6849
  if (r.paths.length) b.paths = r.paths;
@@ -6993,7 +7050,7 @@ var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id ===
6993
7050
  import { spawn as spawn2 } from "child_process";
6994
7051
  import { readFileSync as readFileSync12, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream } from "fs";
6995
7052
  import { finished } from "stream/promises";
6996
- import { join as join13, dirname as dirname6 } from "path";
7053
+ import { join as join14, dirname as dirname6 } from "path";
6997
7054
  import { createHash as createHash2 } from "crypto";
6998
7055
 
6999
7056
  // src/environment.ts
@@ -7408,14 +7465,14 @@ function expandSkillMarkers(text, lookup, opts = {}) {
7408
7465
  }
7409
7466
 
7410
7467
  // src/skills.ts
7411
- import { readFileSync as readFileSync11, readdirSync as readdirSync3, existsSync as existsSync11 } from "fs";
7412
- import { join as join12 } from "path";
7468
+ import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync11 } from "fs";
7469
+ import { join as join13 } from "path";
7413
7470
  import { parse as parseYaml } from "yaml";
7414
7471
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError8, TablationApiError as TablationApiError2 } from "@tablation/client";
7415
7472
 
7416
7473
  // src/agents.ts
7417
7474
  import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
7418
- import { join as join11 } from "path";
7475
+ import { join as join12 } from "path";
7419
7476
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError7, TablationApiError } from "@tablation/client";
7420
7477
  var PERSONA_NAME = {
7421
7478
  dev: "Developer",
@@ -7429,10 +7486,10 @@ var CREW_AGENT_COLUMN = "agent_id";
7429
7486
  var AgentsSyncError = class extends Error {
7430
7487
  };
7431
7488
  function personaDefaultPrompt(promptsDir, role) {
7432
- const briefPath = join11(promptsDir, "personas", `lane-${role}.md`);
7489
+ const briefPath = join12(promptsDir, "personas", `lane-${role}.md`);
7433
7490
  if (!existsSync10(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7434
7491
  if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
7435
- const commonPath = join11(promptsDir, "personas", "common.md");
7492
+ const commonPath = join12(promptsDir, "personas", "common.md");
7436
7493
  if (!existsSync10(commonPath)) return readFileSync10(briefPath, "utf8");
7437
7494
  return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
7438
7495
  }
@@ -7812,9 +7869,9 @@ function parseSkillFile(path) {
7812
7869
  return { name: meta.name, description: meta.description, prompt: body, file: path };
7813
7870
  }
7814
7871
  function listLocalSkills(promptsDir) {
7815
- const dir = join12(promptsDir, "skills");
7872
+ const dir = join13(promptsDir, "skills");
7816
7873
  if (!existsSync11(dir)) return [];
7817
- return readdirSync3(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
7874
+ return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join13(dir, f)));
7818
7875
  }
7819
7876
  async function syncSkills(route, opts) {
7820
7877
  if (!route.resolved) {
@@ -8047,15 +8104,15 @@ var AgentError = class extends Error {
8047
8104
  var AgentSpawnError = class extends AgentError {
8048
8105
  };
8049
8106
  function planAgentRun(o) {
8050
- const briefPath = join13(o.route.promptsDir, "personas", `lane-${o.role}.md`);
8107
+ const briefPath = join14(o.route.promptsDir, "personas", `lane-${o.role}.md`);
8051
8108
  if (!existsSync12(briefPath)) {
8052
8109
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
8053
8110
  }
8054
- const commonPath = join13(o.route.promptsDir, "personas", "common.md");
8111
+ const commonPath = join14(o.route.promptsDir, "personas", "common.md");
8055
8112
  const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync12(commonPath);
8056
8113
  const now = o.now ?? (() => Date.now());
8057
8114
  const routeLabel = routeSlug(o.route.route);
8058
- const digestPath2 = join13(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
8115
+ const digestPath2 = join14(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
8059
8116
  let digest;
8060
8117
  let ageSeconds;
8061
8118
  if (existsSync12(digestPath2)) {
@@ -8085,7 +8142,7 @@ function planAgentRun(o) {
8085
8142
  digest
8086
8143
  });
8087
8144
  const promptSha = createHash2("sha256").update(expanded.text).digest("hex");
8088
- const streamsDir = join13(o.stateDir, "streams");
8145
+ const streamsDir = join14(o.stateDir, "streams");
8089
8146
  const base = `${routeLabel}-${o.role}-${o.cycle}`;
8090
8147
  const seat = resolveSeat(o.ship, o.route, o.role, o.agentPersona ?? (o.agentModel ? { model: o.agentModel } : {}), o.repo);
8091
8148
  const { toolchain, model } = seat;
@@ -8141,8 +8198,8 @@ function planAgentRun(o) {
8141
8198
  digestAttached: digest !== void 0,
8142
8199
  digestAgeSeconds: ageSeconds,
8143
8200
  model,
8144
- streamPath: join13(streamsDir, `${base}.jsonl`),
8145
- eventsPath: join13(streamsDir, `${base}.events.jsonl`),
8201
+ streamPath: join14(streamsDir, `${base}.jsonl`),
8202
+ eventsPath: join14(streamsDir, `${base}.events.jsonl`),
8146
8203
  ticket: o.ticket,
8147
8204
  agentLog,
8148
8205
  promptVersion: o.route.resolved?.agentPersonas?.[o.role]?.historyId,
@@ -8349,12 +8406,12 @@ function spawnAgent(plan, emit) {
8349
8406
 
8350
8407
  // src/run-worktree.ts
8351
8408
  import { existsSync as existsSync14 } from "fs";
8352
- import { dirname as dirname7, join as join15 } from "path";
8409
+ import { dirname as dirname7, join as join16 } from "path";
8353
8410
 
8354
8411
  // src/ports.ts
8355
8412
  import { execFileSync as execFileSync2 } from "child_process";
8356
8413
  import { existsSync as existsSync13 } from "fs";
8357
- import { join as join14 } from "path";
8414
+ import { join as join15 } from "path";
8358
8415
  var run = (cmd, args) => {
8359
8416
  try {
8360
8417
  return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -8399,7 +8456,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
8399
8456
  }
8400
8457
  function worktreeExists(parent, prefix, n) {
8401
8458
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
8402
- if (existsSync13(join14(parent, name, ".git"))) return true;
8459
+ if (existsSync13(join15(parent, name, ".git"))) return true;
8403
8460
  }
8404
8461
  return false;
8405
8462
  }
@@ -8487,7 +8544,7 @@ function planRunWorktree(dir, cfg, t, role) {
8487
8544
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
8488
8545
  return { action: "recut", path: existing.path, branch, base: baseRef };
8489
8546
  }
8490
- const path = join15(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8547
+ const path = join16(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
8491
8548
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
8492
8549
  return { action: "cut-remote", path, branch: located.branch };
8493
8550
  }
@@ -8695,7 +8752,7 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
8695
8752
  }
8696
8753
 
8697
8754
  // src/connect-wizard.ts
8698
- import { delimiter, dirname as dirname8, join as join16 } from "path";
8755
+ import { delimiter, dirname as dirname8, join as join17 } from "path";
8699
8756
  import { accessSync as accessSync2, constants as constants2, existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
8700
8757
  import { stringify } from "yaml";
8701
8758
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
@@ -8718,7 +8775,7 @@ function defaultIsExecutable(p) {
8718
8775
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
8719
8776
  for (const dir of pathEnv.split(delimiter)) {
8720
8777
  if (!dir) continue;
8721
- const candidate = join16(dir, name);
8778
+ const candidate = join17(dir, name);
8722
8779
  if (isExecutable(candidate)) return candidate;
8723
8780
  }
8724
8781
  return void 0;
@@ -8813,8 +8870,8 @@ Wrote ${o.configPath}.
8813
8870
  }
8814
8871
 
8815
8872
  // src/release-run.ts
8816
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync6 } from "fs";
8817
- import { dirname as dirname9, join as join17 } from "path";
8873
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
8874
+ import { dirname as dirname9, join as join18 } from "path";
8818
8875
  import { tmpdir as tmpdir2 } from "os";
8819
8876
 
8820
8877
  // src/release.ts
@@ -9472,7 +9529,7 @@ async function bump(o, size) {
9472
9529
  }
9473
9530
  return printed;
9474
9531
  }
9475
- const files = o.repo.release.versionFiles.map((f) => join17(o.cwd, f));
9532
+ const files = o.repo.release.versionFiles.map((f) => join18(o.cwd, f));
9476
9533
  const first = files[0];
9477
9534
  if (!existsSync16(first)) {
9478
9535
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
@@ -9504,7 +9561,7 @@ async function bump(o, size) {
9504
9561
  function writeChangelog(o, version, entries) {
9505
9562
  const file = o.repo.release.changelog;
9506
9563
  if (!file || entries.length === 0) return;
9507
- const path = join17(o.cwd, file);
9564
+ const path = join18(o.cwd, file);
9508
9565
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
9509
9566
  if (o.dryRun) {
9510
9567
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
@@ -9589,17 +9646,25 @@ async function confirmLastRelease(o, decision) {
9589
9646
  if (!confirmed) o.emit.warn(`${decision.lastTag} is still not confirmed live \u2014 tickets stay verified`);
9590
9647
  return { confirmed, version };
9591
9648
  }
9649
+ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
9650
+ try {
9651
+ run2(cwd, ["worktree", "remove", "--force", dir]);
9652
+ } finally {
9653
+ try {
9654
+ rmSync7(root, { recursive: true, force: true });
9655
+ } finally {
9656
+ run2(cwd, ["worktree", "prune"]);
9657
+ run2(cwd, ["branch", "-D", branch]);
9658
+ }
9659
+ }
9660
+ }
9592
9661
  function openReleaseCheckout(o) {
9593
9662
  const { remote, base } = o.repo.branch;
9594
9663
  const branch = `crew/release-${Date.now()}`;
9595
- const root = mkdtempSync(join17(tmpdir2(), "crew-release-"));
9596
- const dir = join17(root, "checkout");
9597
- const close = () => {
9598
- gitOk(o.cwd, ["worktree", "remove", "--force", dir]);
9599
- rmSync6(root, { recursive: true, force: true });
9600
- gitOk(o.cwd, ["worktree", "prune"]);
9601
- gitOk(o.cwd, ["branch", "-D", branch]);
9602
- };
9664
+ const root = mkdtempSync(join18(tmpdir2(), "crew-release-"));
9665
+ const dir = join18(root, "checkout");
9666
+ o.emit.emit(`release checkout: ${dir}`, { step: "release" });
9667
+ const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
9603
9668
  try {
9604
9669
  git(o.cwd, ["worktree", "add", "-q", "-b", branch, dir, `${remote}/${base}`]);
9605
9670
  } catch (e) {
@@ -9607,10 +9672,10 @@ function openReleaseCheckout(o) {
9607
9672
  throw e;
9608
9673
  }
9609
9674
  for (const f of o.repo.worktrees.copy) {
9610
- const src = join17(o.cwd, f);
9675
+ const src = join18(o.cwd, f);
9611
9676
  if (!existsSync16(src)) continue;
9612
- mkdirSync8(dirname9(join17(dir, f)), { recursive: true });
9613
- copyFileSync(src, join17(dir, f));
9677
+ mkdirSync8(dirname9(join18(dir, f)), { recursive: true });
9678
+ copyFileSync(src, join18(dir, f));
9614
9679
  }
9615
9680
  return { dir, branch, close };
9616
9681
  }
@@ -9630,7 +9695,19 @@ async function runRelease(o) {
9630
9695
  const tagPattern = o.repo.release.tagPattern ?? "v*";
9631
9696
  o.emit.enter("release");
9632
9697
  const fresh = refreshBase(o);
9633
- const decision = decideRelease(o.cwd, o.tickets, o.contract, {
9698
+ let releasable = o.tickets;
9699
+ if (!o.dryRun) {
9700
+ const skipped = /* @__PURE__ */ new Set();
9701
+ for (const t of o.tickets) {
9702
+ if (t.status !== o.contract.statuses.verified) continue;
9703
+ materializeBranchForTicket(o.cwd, o.repo, t, void 0, (problem) => {
9704
+ skipped.add(t.id);
9705
+ o.emit.error(`not releasing ${t.issue_id} this cycle: ${problem}`, { ticket: t.issue_id });
9706
+ });
9707
+ }
9708
+ if (skipped.size) releasable = o.tickets.filter((t) => !skipped.has(t.id));
9709
+ }
9710
+ const decision = decideRelease(o.cwd, releasable, o.contract, {
9634
9711
  tagPattern,
9635
9712
  base: o.repo.branch.base,
9636
9713
  blockerInfo: o.blockerInfo,
@@ -9689,6 +9766,8 @@ async function runRelease(o) {
9689
9766
  let checkout;
9690
9767
  const stranded = recoverStrandedReleaseCheckouts(o.cwd);
9691
9768
  if (stranded.length > 0) o.emit.warn(scoped(`dropped release checkout(s) stranded by a crash: ${stranded.join(", ")}`));
9769
+ const swept = sweepStaleReleaseRoots(tmpdir2());
9770
+ if (swept.length > 0) o.emit.emit(scoped(`removed ${swept.length} stale crew-release-* temp dir(s)`), { step: "release" });
9692
9771
  try {
9693
9772
  checkout = openReleaseCheckout(o);
9694
9773
  } catch (e) {
@@ -11451,14 +11530,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
11451
11530
  }
11452
11531
 
11453
11532
  // src/stream-sweep.ts
11454
- import { existsSync as existsSync19, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync7 } from "fs";
11455
- import { join as join18 } from "path";
11533
+ import { existsSync as existsSync19, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
11534
+ import { join as join19 } from "path";
11456
11535
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
11457
11536
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
11458
- const dir = join18(stateDir, "streams");
11537
+ const dir = join19(stateDir, "streams");
11459
11538
  if (!existsSync19(dir)) return [];
11460
11539
  const cutoff = now - retentionDays * DAY_MS2;
11461
- return readdirSync4(dir).map((name) => join18(dir, name)).filter((path) => {
11540
+ return readdirSync5(dir).map((name) => join19(dir, name)).filter((path) => {
11462
11541
  try {
11463
11542
  return statSync7(path).mtimeMs < cutoff;
11464
11543
  } catch {
@@ -11474,7 +11553,7 @@ function applyStreamSweep(paths, dryRun, log) {
11474
11553
  continue;
11475
11554
  }
11476
11555
  try {
11477
- rmSync7(path, { force: true });
11556
+ rmSync8(path, { force: true });
11478
11557
  removed++;
11479
11558
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
11480
11559
  } catch (e) {
@@ -11486,7 +11565,7 @@ function applyStreamSweep(paths, dryRun, log) {
11486
11565
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
11487
11566
  if (!existsSync19(stateDir)) return [];
11488
11567
  const cutoff = now - retentionDays * DAY_MS2;
11489
- return readdirSync4(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join18(stateDir, name)).filter((path) => {
11568
+ return readdirSync5(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join19(stateDir, name)).filter((path) => {
11490
11569
  try {
11491
11570
  return statSync7(path).mtimeMs < cutoff;
11492
11571
  } catch {
@@ -11503,11 +11582,11 @@ import { dirname as dirOf, resolve as resolvePath } from "path";
11503
11582
  // src/ssh-keys.ts
11504
11583
  import { execFileSync as execFileSync3 } from "child_process";
11505
11584
  import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
11506
- import { dirname as dirname11, join as join19 } from "path";
11585
+ import { dirname as dirname11, join as join20 } from "path";
11507
11586
  var SshKeyError = class extends Error {
11508
11587
  };
11509
11588
  function shipSshKeyPathFor(stateDir) {
11510
- return join19(stateDir, "ssh", "id_ed25519");
11589
+ return join20(stateDir, "ssh", "id_ed25519");
11511
11590
  }
11512
11591
  function ensureShipSshKeypair(stateDir) {
11513
11592
  const privateKeyPath = shipSshKeyPathFor(stateDir);
@@ -11752,9 +11831,9 @@ function syncAllPassengerCheckouts(cfg) {
11752
11831
 
11753
11832
  // src/tunnel.ts
11754
11833
  import { spawn as spawn3 } from "child_process";
11755
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync5 } from "fs";
11834
+ import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync6 } from "fs";
11756
11835
  import { randomInt } from "crypto";
11757
- import { join as join20 } from "path";
11836
+ import { join as join21 } from "path";
11758
11837
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
11759
11838
  var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
11760
11839
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -11821,7 +11900,7 @@ function startTunnel(opts) {
11821
11900
  };
11822
11901
  }
11823
11902
  function tunnelStatePath(stateDir, workspaceId) {
11824
- return join20(stateDir, "passengers", `${workspaceId}.json`);
11903
+ return join21(stateDir, "passengers", `${workspaceId}.json`);
11825
11904
  }
11826
11905
  function readPersistedTunnel(stateDir, workspaceId) {
11827
11906
  const path = tunnelStatePath(stateDir, workspaceId);
@@ -11834,7 +11913,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
11834
11913
  }
11835
11914
  function writePersistedTunnel(stateDir, state) {
11836
11915
  const path = tunnelStatePath(stateDir, state.workspaceId);
11837
- mkdirSync10(join20(stateDir, "passengers"), { recursive: true });
11916
+ mkdirSync10(join21(stateDir, "passengers"), { recursive: true });
11838
11917
  writeFileSync11(path, `${JSON.stringify(state, null, 2)}
11839
11918
  `);
11840
11919
  }
@@ -11932,9 +12011,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
11932
12011
  }
11933
12012
  }
11934
12013
  function readAllPersistedWorkspaceIds(stateDir) {
11935
- const dir = join20(stateDir, "passengers");
12014
+ const dir = join21(stateDir, "passengers");
11936
12015
  if (!existsSync21(dir)) return [];
11937
- return readdirSync5(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
12016
+ return readdirSync6(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
11938
12017
  }
11939
12018
 
11940
12019
  // src/mcp-endpoint-registry.ts
@@ -12054,8 +12133,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
12054
12133
  import { createServer as createHttpServer } from "http";
12055
12134
  import { spawn as spawn4 } from "child_process";
12056
12135
  import { randomBytes } from "crypto";
12057
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync6 } from "fs";
12058
- import { join as join21 } from "path";
12136
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
12137
+ import { join as join22 } from "path";
12059
12138
 
12060
12139
  // src/secret-compare.ts
12061
12140
  import { timingSafeEqual } from "crypto";
@@ -12144,7 +12223,7 @@ var defaultSpawn2 = (cmd, args, env) => {
12144
12223
  return child;
12145
12224
  };
12146
12225
  function syncDaemonStatePath(stateDir, workspaceId) {
12147
- return join21(stateDir, "passengers", `${workspaceId}-sync.json`);
12226
+ return join22(stateDir, "passengers", `${workspaceId}-sync.json`);
12148
12227
  }
12149
12228
  function readPersistedSyncDaemon(stateDir, workspaceId) {
12150
12229
  const path = syncDaemonStatePath(stateDir, workspaceId);
@@ -12157,7 +12236,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
12157
12236
  }
12158
12237
  function writePersistedSyncDaemon(stateDir, state) {
12159
12238
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
12160
- mkdirSync11(join21(stateDir, "passengers"), { recursive: true });
12239
+ mkdirSync11(join22(stateDir, "passengers"), { recursive: true });
12161
12240
  writeFileSync12(path, `${JSON.stringify(state, null, 2)}
12162
12241
  `);
12163
12242
  }
@@ -12178,10 +12257,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
12178
12257
  }
12179
12258
  }
12180
12259
  function readAllPersistedSyncWorkspaceIds(stateDir) {
12181
- const dir = join21(stateDir, "passengers");
12260
+ const dir = join22(stateDir, "passengers");
12182
12261
  if (!existsSync22(dir)) return [];
12183
12262
  const suffix = "-sync.json";
12184
- return readdirSync6(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
12263
+ return readdirSync7(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
12185
12264
  }
12186
12265
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
12187
12266
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -12236,8 +12315,8 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
12236
12315
  }
12237
12316
 
12238
12317
  // src/daemon.ts
12239
- import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync17, readdirSync as readdirSync7, rmSync as rmSync8, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12240
- import { join as join22 } from "path";
12318
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync17, readdirSync as readdirSync8, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12319
+ import { join as join23 } from "path";
12241
12320
  var DEFAULT_FLOOR_MS = 5e3;
12242
12321
  var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
12243
12322
  var defaultSleep = (ms2) => new Promise((resolve5) => setTimeout(resolve5, ms2));
@@ -12275,8 +12354,8 @@ function latestMtime(path) {
12275
12354
  if (stat.isFile()) return stat.mtimeMs;
12276
12355
  if (!stat.isDirectory()) return 0;
12277
12356
  let latest = stat.mtimeMs;
12278
- for (const entry of readdirSync7(path, { withFileTypes: true })) {
12279
- latest = Math.max(latest, latestMtime(join22(path, entry.name)));
12357
+ for (const entry of readdirSync8(path, { withFileTypes: true })) {
12358
+ latest = Math.max(latest, latestMtime(join23(path, entry.name)));
12280
12359
  }
12281
12360
  return latest;
12282
12361
  }
@@ -12288,14 +12367,14 @@ function makeStaleChecker(paths, opts = {}) {
12288
12367
  return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync23(opts.reloadSentinel);
12289
12368
  }
12290
12369
  function reloadSentinelFor(stateDir) {
12291
- return join22(stateDir, "reload-requested");
12370
+ return join23(stateDir, "reload-requested");
12292
12371
  }
12293
12372
  function daemonStampFor(stateDir) {
12294
- return join22(stateDir, "daemon.started");
12373
+ return join23(stateDir, "daemon.started");
12295
12374
  }
12296
12375
  function noteDaemonStarted(stateDir, now = Date.now()) {
12297
12376
  mkdirSync12(stateDir, { recursive: true });
12298
- rmSync8(reloadSentinelFor(stateDir), { force: true });
12377
+ rmSync9(reloadSentinelFor(stateDir), { force: true });
12299
12378
  writeFileSync13(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
12300
12379
  `);
12301
12380
  }
@@ -13115,7 +13194,7 @@ if (command === "passenger-sync-daemon") {
13115
13194
  }
13116
13195
  if (host === "linux" && hasSystemd) {
13117
13196
  const label = labelFor(CREW_HOME, "run");
13118
- const staleTimer = join23(homedir8(), ".config", "systemd", "user", `${label}.timer`);
13197
+ const staleTimer = join24(homedir8(), ".config", "systemd", "user", `${label}.timer`);
13119
13198
  if (existsSync24(staleTimer)) {
13120
13199
  process.stdout.write(
13121
13200
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
@@ -14123,7 +14202,7 @@ ${changed2.map((c) => ` - ${c}
14123
14202
  process.exit(1);
14124
14203
  }
14125
14204
  requireArmed("run the daemon loop");
14126
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join23(CREW_HOME, "src"), join23(CREW_HOME, "dist"), join23(CREW_HOME, "bin")];
14205
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join24(CREW_HOME, "src"), join24(CREW_HOME, "dist"), join24(CREW_HOME, "bin")];
14127
14206
  const configPaths = daemonConfigPaths(cfg);
14128
14207
  watchPaths.push(...configPaths);
14129
14208
  noteDaemonStarted(cfg.ship.stateDir);
@@ -14803,7 +14882,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
14803
14882
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
14804
14883
  const wizard = firstRun;
14805
14884
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
14806
- const routeDir = value("dir") ?? (writeFirstRun ? join23(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
14885
+ const routeDir = value("dir") ?? (writeFirstRun ? join24(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
14807
14886
  const routeBlock = renderConnection(
14808
14887
  found,
14809
14888
  route2,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.47.1",
3
+ "version": "0.47.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",