@tablation/crew 0.60.0 → 0.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +8 -0
  2. package/dist/cli.js +678 -348
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2332,6 +2332,7 @@ function parseResolved(raw, m, where) {
2332
2332
  ...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
2333
2333
  ...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
2334
2334
  ...typeof raw.role === "string" && raw.role ? { role: raw.role } : {},
2335
+ ...Array.isArray(raw.capabilityMisses) && raw.capabilityMisses.length > 0 ? { capabilityMisses: raw.capabilityMisses.filter((x) => typeof x === "string") } : {},
2335
2336
  ...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
2336
2337
  ...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
2337
2338
  };
@@ -5182,10 +5183,11 @@ function doctorSelftestLines(stored, toolchains) {
5182
5183
  }
5183
5184
 
5184
5185
  // src/cli.ts
5185
- import { resolve as resolve5, dirname as dirname16, join as join29, basename as basename10 } from "path";
5186
+ import { resolve as resolve5, dirname as dirname17, join as join30, basename as basename10 } from "path";
5186
5187
  import { fileURLToPath } from "url";
5187
5188
  import { hostname as hostname2, homedir as homedir8 } from "os";
5188
5189
  import { spawnSync as spawnSync6 } from "child_process";
5190
+ import { parse as parseYaml2 } from "yaml";
5189
5191
 
5190
5192
  // src/argv.ts
5191
5193
  var VALUE_FLAGS = /* @__PURE__ */ new Set([
@@ -5760,154 +5762,105 @@ var State = class _State {
5760
5762
  }
5761
5763
  };
5762
5764
 
5763
- // src/tracker.ts
5764
- import { hostname } from "os";
5765
- import { TablationClient as TablationClient3 } from "@tablation/client";
5766
-
5767
- // src/contract.ts
5768
- var DEFAULT_CONTRACT = {
5769
- columns: {
5770
- key: "issue_id",
5771
- tag: "issue_tag",
5772
- title: "title",
5773
- status: "status",
5774
- assignee: "assignee_id",
5775
- priority: "priority",
5776
- severity: "severity",
5777
- slice: "project_id",
5778
- repo: "repo_id",
5779
- parent: "parent_id",
5780
- blockedBy: "blocked_by",
5781
- updatedAt: "updated_at",
5782
- needsDesign: "needs_design",
5783
- attachments: "attachments",
5784
- workMode: "work_mode",
5785
- reportType: "report_type",
5786
- epic: "epic_id",
5787
- author: "filed_by_id",
5788
- ship: "ship_id",
5789
- heldBy: "held_by_ship_id",
5790
- resolutionNote: "resolution_note",
5791
- verifiedSha: "verified_sha",
5792
- prRef: "pr_ref",
5793
- position: "position"
5794
- },
5795
- comments: {
5796
- parent: "ticket_id",
5797
- body: "body",
5798
- author: "team_member_id",
5799
- createdAt: "created_at",
5800
- ship: "ship_id",
5801
- publicReply: "is_public_reply",
5802
- authorName: "author_name"
5803
- },
5804
- statuses: {
5805
- open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
5806
- resolved: [
5807
- "verified",
5808
- "closed_deployed",
5809
- "closed_wont_fix",
5810
- "closed_duplicate",
5811
- "closed_completed",
5812
- "closed_cant_reproduce"
5813
- ],
5814
- approved: "accepted",
5815
- parked: "blocked",
5816
- building: "in_progress",
5817
- handoff: "fixed",
5818
- verifying: "qa",
5819
- verified: "verified",
5820
- deployed: "closed_deployed",
5821
- needsHuman: "needs_info",
5822
- reviewing: null,
5823
- wontFix: "closed_wont_fix",
5824
- draft: "draft"
5825
- },
5826
- epics: {
5827
- statusColumn: "status",
5828
- keyColumn: "epic_id",
5829
- statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
5830
- },
5831
- priorityOrder: ["p0", "p1", "p2", "p3"],
5832
- severityOrder: ["s1", "s2", "s3", "s4"],
5833
- unknownPriorityRank: 2,
5834
- coordinatingValue: "coordinating",
5835
- manualWorkMode: "manual",
5836
- hybridWorkMode: "hybrid",
5837
- repoExemptReportTypes: ["coordinating", "question", "investigation"]
5838
- };
5839
- function closedStatuses(c) {
5840
- return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
5765
+ // src/reset.ts
5766
+ import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync13, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
5767
+ import { dirname as dirname6, join as join14 } from "path";
5768
+ function clonedCheckoutsFile(stateDir) {
5769
+ return join14(stateDir, "cloned-checkouts.json");
5841
5770
  }
5842
- function resolveContract(override) {
5843
- if (!override) return DEFAULT_CONTRACT;
5844
- return {
5845
- columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
5846
- comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
5847
- statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
5848
- epics: {
5849
- ...DEFAULT_CONTRACT.epics,
5850
- ...override.epics,
5851
- statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
5852
- },
5853
- priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
5854
- severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
5855
- unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
5856
- coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
5857
- manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
5858
- hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
5859
- // A workspace overriding `coordinatingValue` alone (renaming the rollup
5860
- // marker) still gets that renamed value exempted here by default,
5861
- // without also having to repeat it — only an explicit
5862
- // `repoExemptReportTypes` overrides this derived default.
5863
- repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
5864
- };
5865
- }
5866
- function validateContract(c) {
5867
- const problems = [];
5868
- const open = new Set(c.statuses.open);
5869
- const resolved = new Set(c.statuses.resolved);
5870
- for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
5871
- const v = c.statuses[role];
5872
- if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
5771
+ function readClonedCheckouts(stateDir) {
5772
+ try {
5773
+ const parsed = JSON.parse(readFileSync13(clonedCheckoutsFile(stateDir), "utf8"));
5774
+ return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
5775
+ } catch {
5776
+ return [];
5873
5777
  }
5874
- if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
5875
- problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
5778
+ }
5779
+ function recordClonedCheckout(stateDir, dir) {
5780
+ const known = readClonedCheckouts(stateDir);
5781
+ if (known.includes(dir)) return;
5782
+ const file = clonedCheckoutsFile(stateDir);
5783
+ mkdirSync7(dirname6(file), { recursive: true });
5784
+ writeFileSync8(file, `${JSON.stringify([...known, dir], null, 2)}
5785
+ `);
5786
+ }
5787
+ var mark = (dryRun) => dryRun ? "would-remove" : "removed";
5788
+ async function runReset(o, fx) {
5789
+ const steps = [];
5790
+ for (const line of await fx.uninstallUnits(o.dryRun)) steps.push({ step: "scheduler", target: line, action: mark(o.dryRun) });
5791
+ const urls = [...new Set(o.routes.map((r) => r.baseUrl))];
5792
+ for (const url of urls) {
5793
+ for (const line of fx.removePair(url, o.dryRun)) steps.push({ step: "pair", target: line, action: mark(o.dryRun) });
5876
5794
  }
5877
- if (!resolved.has(c.statuses.deployed)) {
5878
- problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
5795
+ for (const route of o.routes) {
5796
+ for (const r of await fx.revokeSeatKeys(route, o.dryRun)) {
5797
+ steps.push({ step: "seat-keys", target: `${route.route}: ${r.line}`, action: r.ok ? mark(o.dryRun) : "failed" });
5798
+ }
5879
5799
  }
5880
- if (resolved.has(c.statuses.handoff)) {
5881
- problems.push(
5882
- `statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
5883
- );
5800
+ for (const route of [...o.routes, ...o.droppedRoutes ?? []]) {
5801
+ try {
5802
+ const cred = await fx.credential(route);
5803
+ if (!cred.stored) {
5804
+ steps.push({ step: "credential", target: route.route, action: "absent", detail: "no workspace credential in the keychain" });
5805
+ continue;
5806
+ }
5807
+ if (!o.dryRun) await cred.clear();
5808
+ steps.push({ step: "credential", target: route.route, action: mark(o.dryRun), detail: "workspace credential in the keychain" });
5809
+ } catch (e) {
5810
+ steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
5811
+ }
5884
5812
  }
5885
- if (!resolved.has(c.statuses.wontFix)) {
5886
- problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
5813
+ if (o.removeCheckouts) {
5814
+ for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
5887
5815
  }
5888
- if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
5889
- problems.push(
5890
- `statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
5891
- );
5816
+ steps.push(...removeState(o.stateDir, o.dryRun));
5817
+ if (existsSync14(o.configFile)) {
5818
+ if (!o.dryRun) rmSync7(o.configFile, { force: true });
5819
+ steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
5820
+ } else {
5821
+ steps.push({ step: "config", target: o.configFile, action: "absent" });
5892
5822
  }
5893
- if (resolved.has(c.statuses.approved)) {
5894
- problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
5823
+ return steps;
5824
+ }
5825
+ function removeCheckout(dir, dryRun, fx) {
5826
+ if (!existsSync14(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
5827
+ const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
5828
+ if (dirty.length) {
5829
+ return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
5895
5830
  }
5896
- if (c.statuses.parked === c.statuses.approved) {
5897
- problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
5831
+ if (linkedWorktrees.length) {
5832
+ return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
5898
5833
  }
5899
- const epicValues = Object.values(c.epics.statuses);
5900
- if (new Set(epicValues).size !== epicValues.length) {
5901
- problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
5834
+ if (!dryRun) rmSync7(dir, { recursive: true, force: true });
5835
+ return { step: "checkout", target: dir, action: mark(dryRun) };
5836
+ }
5837
+ function removeState(stateDir, dryRun) {
5838
+ const out = [];
5839
+ const targets = [join14(stateDir, "resolved"), join14(stateDir, "keys"), clonedCheckoutsFile(stateDir), join14(stateDir, "reload-requested"), join14(stateDir, "daemon.started")];
5840
+ try {
5841
+ for (const name of readdirSync4(stateDir)) {
5842
+ if (name.startsWith(".") && statSync5(join14(stateDir, name)).isFile()) targets.push(join14(stateDir, name));
5843
+ }
5844
+ } catch {
5902
5845
  }
5903
- if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
5904
- if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
5905
- for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
5906
- if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
5846
+ for (const t of targets) {
5847
+ if (!existsSync14(t)) continue;
5848
+ if (!dryRun) rmSync7(t, { recursive: true, force: true });
5849
+ out.push({ step: "state", target: t, action: mark(dryRun) });
5907
5850
  }
5908
- return problems;
5851
+ if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
5852
+ return out;
5853
+ }
5854
+ function describeResetStep(s) {
5855
+ const verb = { removed: "removed", "would-remove": "would remove", absent: "nothing to remove", kept: "KEPT", failed: "FAILED" }[s.action];
5856
+ return `${s.step}: ${verb}: ${s.target}${s.detail ? ` (${s.detail})` : ""}`;
5857
+ }
5858
+ function resetIncomplete(steps) {
5859
+ return steps.some((s) => s.action === "failed" || s.action === "kept");
5909
5860
  }
5910
- var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
5861
+
5862
+ // src/crew-floor.ts
5863
+ import { TablationApiError as TablationApiError3 } from "@tablation/client";
5911
5864
 
5912
5865
  // src/callout.ts
5913
5866
  var calloutHeader = (kind, label) => `> [!${kind}]
@@ -6154,24 +6107,24 @@ function recoveredFailureHeadlines(o, route) {
6154
6107
  }
6155
6108
 
6156
6109
  // src/ship-attention.ts
6157
- import { existsSync as existsSync15, statfsSync } from "fs";
6110
+ import { existsSync as existsSync16, statfsSync } from "fs";
6158
6111
 
6159
6112
  // src/git.ts
6160
6113
  import { execFileSync as execFileSync2 } from "child_process";
6161
6114
  import {
6162
- existsSync as existsSync14,
6163
- mkdirSync as mkdirSync7,
6115
+ existsSync as existsSync15,
6116
+ mkdirSync as mkdirSync8,
6164
6117
  openSync as openSync3,
6165
6118
  closeSync as closeSync3,
6166
6119
  writeSync,
6167
- statSync as statSync5,
6120
+ statSync as statSync6,
6168
6121
  unlinkSync as unlinkSync2,
6169
- readFileSync as readFileSync13,
6170
- writeFileSync as writeFileSync8,
6171
- readdirSync as readdirSync4,
6172
- rmSync as rmSync7
6122
+ readFileSync as readFileSync14,
6123
+ writeFileSync as writeFileSync9,
6124
+ readdirSync as readdirSync5,
6125
+ rmSync as rmSync8
6173
6126
  } from "fs";
6174
- import { dirname as dirname6, basename as basename5, join as join14 } from "path";
6127
+ import { dirname as dirname7, basename as basename5, join as join15 } from "path";
6175
6128
  var GitError = class extends Error {
6176
6129
  };
6177
6130
  function streamText(x) {
@@ -6537,8 +6490,8 @@ function recoverStrandedReleaseCheckouts(cwd) {
6537
6490
  if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
6538
6491
  else if (line.startsWith("branch refs/heads/crew/release-") && path) {
6539
6492
  gitOk(cwd, ["worktree", "remove", "--force", path]);
6540
- const root = dirname6(path);
6541
- if (basename5(root).startsWith("crew-release-")) rmSync7(root, { recursive: true, force: true });
6493
+ const root = dirname7(path);
6494
+ if (basename5(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
6542
6495
  }
6543
6496
  }
6544
6497
  const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
@@ -6549,18 +6502,18 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
6549
6502
  const removed = [];
6550
6503
  let names;
6551
6504
  try {
6552
- names = readdirSync4(tmp);
6505
+ names = readdirSync5(tmp);
6553
6506
  } catch {
6554
6507
  return removed;
6555
6508
  }
6556
6509
  for (const name of names) {
6557
6510
  if (!name.startsWith("crew-release-")) continue;
6558
- const root = join14(tmp, name);
6511
+ const root = join15(tmp, name);
6559
6512
  try {
6560
- const st = statSync5(root);
6513
+ const st = statSync6(root);
6561
6514
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
6562
- if (existsSync14(join14(root, "checkout"))) continue;
6563
- rmSync7(root, { recursive: true, force: true });
6515
+ if (existsSync15(join15(root, "checkout"))) continue;
6516
+ rmSync8(root, { recursive: true, force: true });
6564
6517
  removed.push(name);
6565
6518
  } catch {
6566
6519
  }
@@ -6602,13 +6555,13 @@ function originUrlOf(dir, remoteName) {
6602
6555
  }
6603
6556
  }
6604
6557
  function ensureRepoCheckout(dir, remote, opts = {}) {
6605
- if (existsSync14(`${dir}/.git`)) return false;
6558
+ if (existsSync15(`${dir}/.git`)) return false;
6606
6559
  if (!remote) {
6607
6560
  throw new GitError(
6608
6561
  `${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
6609
6562
  );
6610
6563
  }
6611
- mkdirSync7(dirname6(dir), { recursive: true });
6564
+ mkdirSync8(dirname7(dir), { recursive: true });
6612
6565
  const url = cloneUrlFor(remote, opts);
6613
6566
  try {
6614
6567
  execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
@@ -6617,8 +6570,13 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
6617
6570
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
6618
6571
  throw new GitError(`git clone ${url} ${dir}: ${why || err.message.trim()}`);
6619
6572
  }
6573
+ cloneRecorder?.(dir);
6620
6574
  return true;
6621
6575
  }
6576
+ var cloneRecorder;
6577
+ function setCloneRecorder(fn) {
6578
+ cloneRecorder = fn;
6579
+ }
6622
6580
  var CHECKOUT_LOCK_STALE_MS = 5 * 60 * 1e3;
6623
6581
  function checkoutLockPath(dir) {
6624
6582
  return `${dir}.sync.lock`;
@@ -6628,7 +6586,7 @@ function sleepSync(ms2) {
6628
6586
  }
6629
6587
  function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6630
6588
  const lockPath = checkoutLockPath(dir);
6631
- mkdirSync7(dirname6(lockPath), { recursive: true });
6589
+ mkdirSync8(dirname7(lockPath), { recursive: true });
6632
6590
  const deadline = Date.now() + timeoutMs;
6633
6591
  for (; ; ) {
6634
6592
  try {
@@ -6646,7 +6604,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6646
6604
  const err = e;
6647
6605
  if (err.code !== "EEXIST") throw err;
6648
6606
  try {
6649
- if (Date.now() - statSync5(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
6607
+ if (Date.now() - statSync6(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
6650
6608
  try {
6651
6609
  unlinkSync2(lockPath);
6652
6610
  } catch {
@@ -6665,17 +6623,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
6665
6623
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
6666
6624
  const release = acquireCheckoutLock(dir);
6667
6625
  try {
6668
- if (!existsSync14(`${dir}/.git`) && githubRemote) {
6626
+ if (!existsSync15(`${dir}/.git`) && githubRemote) {
6669
6627
  const url = cloneUrlFor(githubRemote, clone);
6670
6628
  const marker = cloneFailureMarker(dir);
6671
- if (existsSync14(marker) && readFileSync13(marker, "utf8").split("\n")[0] === url) {
6672
- return { action: "clone-failed", detail: readFileSync13(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
6629
+ if (existsSync15(marker) && readFileSync14(marker, "utf8").split("\n")[0] === url) {
6630
+ return { action: "clone-failed", detail: readFileSync14(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
6673
6631
  }
6674
6632
  try {
6675
6633
  ensureRepoCheckout(dir, githubRemote, clone);
6676
6634
  } catch (e) {
6677
6635
  if (!(e instanceof GitError)) throw e;
6678
- writeFileSync8(marker, `${url}
6636
+ writeFileSync9(marker, `${url}
6679
6637
  ${e.message}`);
6680
6638
  return { action: "clone-failed", detail: e.message, repeat: false };
6681
6639
  }
@@ -6873,7 +6831,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
6873
6831
  function reconcileHeldWorktrees(state, repo, log, stillRefused) {
6874
6832
  for (const i of state.shipAttention().previous()) {
6875
6833
  if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
6876
- const gone = !existsSync15(i.held.path);
6834
+ const gone = !existsSync16(i.held.path);
6877
6835
  if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
6878
6836
  clearShipAttention(state, i.key);
6879
6837
  log.emit(
@@ -6951,6 +6909,223 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
6951
6909
  "ok"
6952
6910
  ]);
6953
6911
 
6912
+ // src/crew-floor.ts
6913
+ var CREW_OUTDATED_KEY = "crew_outdated";
6914
+ function parseVersion(v) {
6915
+ const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
6916
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
6917
+ }
6918
+ function compareVersions(a, b) {
6919
+ const pa = parseVersion(a);
6920
+ const pb = parseVersion(b);
6921
+ if (!pa || !pb) return null;
6922
+ for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
6923
+ return 0;
6924
+ }
6925
+ function belowFloor(have, floor) {
6926
+ if (!floor) return false;
6927
+ const c = compareVersions(have, floor);
6928
+ return c !== null && c < 0;
6929
+ }
6930
+ function outdatedItem(have, floor, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
6931
+ return {
6932
+ kind: "crew_outdated",
6933
+ key: CREW_OUTDATED_KEY,
6934
+ since: nowIso,
6935
+ message: `crew ${have} is below this workspace's minimum ${floor} \u2014 no ticket will be picked until it is updated (the Crew app updates it, or \`npm install -g @tablation/crew\`). Resumes automatically once the version satisfies the floor`
6936
+ };
6937
+ }
6938
+ function sentVersion(ship) {
6939
+ return /CrewAgent\/(\d+\.\d+\.\d+\S*)/.exec(ship.userAgent)?.[1] ?? null;
6940
+ }
6941
+ async function readCrewFloor(route, ship) {
6942
+ const r = route.resolved;
6943
+ if (!r?.projectId) return void 0;
6944
+ try {
6945
+ const res = await fetch(`${route.baseUrl}/api/projects/${encodeURIComponent(r.projectId)}?workspaceId=${r.workspaceId}`, {
6946
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": ship.userAgent }
6947
+ });
6948
+ if (!res.ok) return void 0;
6949
+ const body = await res.json();
6950
+ return body.crewMinVersion ?? null;
6951
+ } catch {
6952
+ return void 0;
6953
+ }
6954
+ }
6955
+ async function checkCrewFloor(o, deps = {}) {
6956
+ const have = deps.version ?? sentVersion(o.ship);
6957
+ if (!have) return null;
6958
+ const floor = await (deps.read ?? readCrewFloor)(o.route, o.ship);
6959
+ if (floor === void 0) return openOutdated(o.state);
6960
+ if (belowFloor(have, floor)) {
6961
+ const item = outdatedItem(have, floor);
6962
+ await raiseShipAttention(o, item);
6963
+ return item.message;
6964
+ }
6965
+ clearShipAttention(o.state, CREW_OUTDATED_KEY);
6966
+ return null;
6967
+ }
6968
+ function openOutdated(state) {
6969
+ return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
6970
+ }
6971
+ function isUpgradeRequired(err) {
6972
+ return err instanceof TablationApiError3 && err.status === 426;
6973
+ }
6974
+ async function raiseFromUpgradeRequired(o, err) {
6975
+ const have = sentVersion(o.ship) ?? "unknown";
6976
+ const floor = /(\d+\.\d+\.\d+)/.exec(err.message.replace(have, ""))?.[1] ?? "a newer version";
6977
+ await raiseShipAttention(o, outdatedItem(have, floor));
6978
+ }
6979
+
6980
+ // src/tracker.ts
6981
+ import { hostname } from "os";
6982
+ import { TablationClient as TablationClient3 } from "@tablation/client";
6983
+
6984
+ // src/contract.ts
6985
+ var DEFAULT_CONTRACT = {
6986
+ columns: {
6987
+ key: "issue_id",
6988
+ tag: "issue_tag",
6989
+ title: "title",
6990
+ status: "status",
6991
+ assignee: "assignee_id",
6992
+ priority: "priority",
6993
+ severity: "severity",
6994
+ slice: "project_id",
6995
+ repo: "repo_id",
6996
+ parent: "parent_id",
6997
+ blockedBy: "blocked_by",
6998
+ updatedAt: "updated_at",
6999
+ needsDesign: "needs_design",
7000
+ attachments: "attachments",
7001
+ workMode: "work_mode",
7002
+ reportType: "report_type",
7003
+ epic: "epic_id",
7004
+ author: "filed_by_id",
7005
+ ship: "ship_id",
7006
+ heldBy: "held_by_ship_id",
7007
+ resolutionNote: "resolution_note",
7008
+ verifiedSha: "verified_sha",
7009
+ prRef: "pr_ref",
7010
+ position: "position"
7011
+ },
7012
+ comments: {
7013
+ parent: "ticket_id",
7014
+ body: "body",
7015
+ author: "team_member_id",
7016
+ createdAt: "created_at",
7017
+ ship: "ship_id",
7018
+ publicReply: "is_public_reply",
7019
+ authorName: "author_name"
7020
+ },
7021
+ statuses: {
7022
+ open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
7023
+ resolved: [
7024
+ "verified",
7025
+ "closed_deployed",
7026
+ "closed_wont_fix",
7027
+ "closed_duplicate",
7028
+ "closed_completed",
7029
+ "closed_cant_reproduce"
7030
+ ],
7031
+ approved: "accepted",
7032
+ parked: "blocked",
7033
+ building: "in_progress",
7034
+ handoff: "fixed",
7035
+ verifying: "qa",
7036
+ verified: "verified",
7037
+ deployed: "closed_deployed",
7038
+ needsHuman: "needs_info",
7039
+ reviewing: null,
7040
+ wontFix: "closed_wont_fix",
7041
+ draft: "draft"
7042
+ },
7043
+ epics: {
7044
+ statusColumn: "status",
7045
+ keyColumn: "epic_id",
7046
+ statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
7047
+ },
7048
+ priorityOrder: ["p0", "p1", "p2", "p3"],
7049
+ severityOrder: ["s1", "s2", "s3", "s4"],
7050
+ unknownPriorityRank: 2,
7051
+ coordinatingValue: "coordinating",
7052
+ manualWorkMode: "manual",
7053
+ hybridWorkMode: "hybrid",
7054
+ repoExemptReportTypes: ["coordinating", "question", "investigation"]
7055
+ };
7056
+ function closedStatuses(c) {
7057
+ return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
7058
+ }
7059
+ function resolveContract(override) {
7060
+ if (!override) return DEFAULT_CONTRACT;
7061
+ return {
7062
+ columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
7063
+ comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
7064
+ statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
7065
+ epics: {
7066
+ ...DEFAULT_CONTRACT.epics,
7067
+ ...override.epics,
7068
+ statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
7069
+ },
7070
+ priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
7071
+ severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
7072
+ unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
7073
+ coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
7074
+ manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
7075
+ hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
7076
+ // A workspace overriding `coordinatingValue` alone (renaming the rollup
7077
+ // marker) still gets that renamed value exempted here by default,
7078
+ // without also having to repeat it — only an explicit
7079
+ // `repoExemptReportTypes` overrides this derived default.
7080
+ repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
7081
+ };
7082
+ }
7083
+ function validateContract(c) {
7084
+ const problems = [];
7085
+ const open = new Set(c.statuses.open);
7086
+ const resolved = new Set(c.statuses.resolved);
7087
+ for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
7088
+ const v = c.statuses[role];
7089
+ if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
7090
+ }
7091
+ if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
7092
+ problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
7093
+ }
7094
+ if (!resolved.has(c.statuses.deployed)) {
7095
+ problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
7096
+ }
7097
+ if (resolved.has(c.statuses.handoff)) {
7098
+ problems.push(
7099
+ `statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
7100
+ );
7101
+ }
7102
+ if (!resolved.has(c.statuses.wontFix)) {
7103
+ problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
7104
+ }
7105
+ if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
7106
+ problems.push(
7107
+ `statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
7108
+ );
7109
+ }
7110
+ if (resolved.has(c.statuses.approved)) {
7111
+ problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
7112
+ }
7113
+ if (c.statuses.parked === c.statuses.approved) {
7114
+ problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
7115
+ }
7116
+ const epicValues = Object.values(c.epics.statuses);
7117
+ if (new Set(epicValues).size !== epicValues.length) {
7118
+ problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
7119
+ }
7120
+ if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
7121
+ if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
7122
+ for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
7123
+ if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
7124
+ }
7125
+ return problems;
7126
+ }
7127
+ var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
7128
+
6954
7129
  // src/work-mode.ts
6955
7130
  function projectWorkModes(rows, contract) {
6956
7131
  const out = /* @__PURE__ */ new Map();
@@ -8587,8 +8762,8 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
8587
8762
  }
8588
8763
 
8589
8764
  // src/poll.ts
8590
- import { writeFileSync as writeFileSync9 } from "fs";
8591
- import { join as join15 } from "path";
8765
+ import { writeFileSync as writeFileSync10 } from "fs";
8766
+ import { join as join16 } from "path";
8592
8767
 
8593
8768
  // src/stalled.ts
8594
8769
  var STALLED_AFTER_CYCLES = 30;
@@ -8824,8 +8999,10 @@ async function decideCycle(o) {
8824
8999
  emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
8825
9000
  }
8826
9001
  await checkRepoDisks({ state, emit, route, ship });
9002
+ const floorStop = await checkCrewFloor({ state, emit, route, ship });
9003
+ const itemStop = repoStopFor(openShipAttention(state), route);
8827
9004
  const selectionInput = {
8828
- repoStop: repoStopFor(openShipAttention(state), route),
9005
+ repoStop: floorStop ? () => floorStop : itemStop,
8829
9006
  workModes,
8830
9007
  tickets,
8831
9008
  comments,
@@ -8877,7 +9054,7 @@ async function decideCycle(o) {
8877
9054
  }
8878
9055
  return decision;
8879
9056
  }
8880
- var digestPath = (stateDir, route, role) => join15(stateDir, `digest-${route}-${role}.md`);
9057
+ var digestPath = (stateDir, route, role) => join16(stateDir, `digest-${route}-${role}.md`);
8881
9058
  function writeDigest(o, d, role, stateDir) {
8882
9059
  const me = o.route.resolved?.seats[role];
8883
9060
  if (!me) return false;
@@ -8941,7 +9118,7 @@ function writeDigest(o, d, role, stateDir) {
8941
9118
  };
8942
9119
  const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
8943
9120
  const path = digestPath(stateDir, routeSlug(o.route.route), role);
8944
- writeFileSync9(path, text);
9121
+ writeFileSync10(path, text);
8945
9122
  o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
8946
9123
  data: { role, bytes: Buffer.byteLength(text) }
8947
9124
  });
@@ -9318,13 +9495,13 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
9318
9495
  }
9319
9496
 
9320
9497
  // src/run-worktree.ts
9321
- import { existsSync as existsSync17 } from "fs";
9322
- import { dirname as dirname7, join as join17 } from "path";
9498
+ import { existsSync as existsSync18 } from "fs";
9499
+ import { dirname as dirname8, join as join18 } from "path";
9323
9500
 
9324
9501
  // src/ports.ts
9325
9502
  import { execFileSync as execFileSync3 } from "child_process";
9326
- import { existsSync as existsSync16 } from "fs";
9327
- import { join as join16 } from "path";
9503
+ import { existsSync as existsSync17 } from "fs";
9504
+ import { join as join17 } from "path";
9328
9505
  var run = (cmd, args) => {
9329
9506
  try {
9330
9507
  return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
@@ -9369,7 +9546,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
9369
9546
  }
9370
9547
  function worktreeExists(parent, prefix, n) {
9371
9548
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
9372
- if (existsSync16(join16(parent, name, ".git"))) return true;
9549
+ if (existsSync17(join17(parent, name, ".git"))) return true;
9373
9550
  }
9374
9551
  return false;
9375
9552
  }
@@ -9445,7 +9622,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9445
9622
  const existing = worktreeForNumber(dir, num);
9446
9623
  const located = locateBranchForTicket(dir, cfg, t, role);
9447
9624
  const baseRef = hasRemote ? `${remote}/${base}` : base;
9448
- if (existing && !existing.prunable && existsSync17(existing.path)) {
9625
+ if (existing && !existing.prunable && existsSync18(existing.path)) {
9449
9626
  const branch = existing.branch;
9450
9627
  if (status(existing.path).length > 0) {
9451
9628
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -9461,7 +9638,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
9461
9638
  if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
9462
9639
  return { action: "recut", path: existing.path, branch, base: baseRef };
9463
9640
  }
9464
- const path = join17(dirname7(dir), worktreeForTicket(dir, cfg, t, role));
9641
+ const path = join18(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
9465
9642
  if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
9466
9643
  return { action: "cut-remote", path, branch: located.branch };
9467
9644
  }
@@ -9510,7 +9687,7 @@ function worktreeHoldsUnpushed(path, remote) {
9510
9687
  }
9511
9688
  async function finishRunWorktree(dir, cfg, branch) {
9512
9689
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
9513
- if (!wt || !existsSync17(wt.path)) return { kind: "none" };
9690
+ if (!wt || !existsSync18(wt.path)) return { kind: "none" };
9514
9691
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
9515
9692
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
9516
9693
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -9522,7 +9699,7 @@ async function finishRunWorktree(dir, cfg, branch) {
9522
9699
  if (pids.length) await killGently(pids);
9523
9700
  }
9524
9701
  gitOk(dir, ["worktree", "remove", wt.path]);
9525
- return existsSync17(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
9702
+ return existsSync18(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
9526
9703
  }
9527
9704
 
9528
9705
  // src/handoff-guard.ts
@@ -9783,13 +9960,13 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
9783
9960
  }
9784
9961
 
9785
9962
  // src/connect-wizard.ts
9786
- import { delimiter, dirname as dirname8, join as join18 } from "path";
9787
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync18, mkdirSync as mkdirSync8, writeFileSync as writeFileSync10 } from "fs";
9963
+ import { delimiter, dirname as dirname9, join as join19 } from "path";
9964
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync19, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
9788
9965
  import { stringify } from "yaml";
9789
9966
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
9790
9967
  function shouldRunWizard(opts) {
9791
9968
  if (!opts.isTTY) return false;
9792
- const exists = opts.configExists ?? existsSync18;
9969
+ const exists = opts.configExists ?? existsSync19;
9793
9970
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
9794
9971
  }
9795
9972
  function firstRunConfigPath() {
@@ -9806,7 +9983,7 @@ function defaultIsExecutable(p) {
9806
9983
  function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
9807
9984
  for (const dir of pathEnv.split(delimiter)) {
9808
9985
  if (!dir) continue;
9809
- const candidate = join18(dir, name);
9986
+ const candidate = join19(dir, name);
9810
9987
  if (isExecutable(candidate)) return candidate;
9811
9988
  }
9812
9989
  return void 0;
@@ -9832,9 +10009,9 @@ async function runWizard(p, env) {
9832
10009
  if (entered && isExec(entered)) agentBin = entered;
9833
10010
  else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
9834
10011
  }
9835
- const nodeDir = dirname8(env.nodePath);
10012
+ const nodeDir = dirname9(env.nodePath);
9836
10013
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
9837
- const baseDirs = [nodeDir, pnpmPath ? dirname8(pnpmPath) : void 0];
10014
+ const baseDirs = [nodeDir, pnpmPath ? dirname9(pnpmPath) : void 0];
9838
10015
  p.say(
9839
10016
  `
9840
10017
  Host Passengers lets this machine run the agent sessions that other people's
@@ -9845,7 +10022,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
9845
10022
  p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
9846
10023
  }
9847
10024
  const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
9848
- const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname8(dockerPath) : void 0]);
10025
+ const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname9(dockerPath) : void 0]);
9849
10026
  if (extraPath) {
9850
10027
  p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
9851
10028
  }
@@ -9867,9 +10044,9 @@ routes:
9867
10044
  ${routeBlock}`;
9868
10045
  }
9869
10046
  function writeNewConfig(path, text) {
9870
- mkdirSync8(dirname8(path), { recursive: true });
10047
+ mkdirSync9(dirname9(path), { recursive: true });
9871
10048
  try {
9872
- writeFileSync10(path, text, { mode: 384, flag: "wx" });
10049
+ writeFileSync11(path, text, { mode: 384, flag: "wx" });
9873
10050
  return true;
9874
10051
  } catch (e) {
9875
10052
  if (e.code === "EEXIST") return false;
@@ -9902,7 +10079,7 @@ Wrote ${o.configPath}.
9902
10079
 
9903
10080
  // src/connect-json.ts
9904
10081
  import { accessSync as accessSync3, constants as constants3 } from "fs";
9905
- import { dirname as dirname9 } from "path";
10082
+ import { dirname as dirname10 } from "path";
9906
10083
  var CHOICE_EXIT_CODE = 3;
9907
10084
  function createEmitter(json, write) {
9908
10085
  return {
@@ -9950,7 +10127,7 @@ function flagWizardAnswers(f, env) {
9950
10127
  if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
9951
10128
  const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
9952
10129
  const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
9953
- const extraPath = f.extraPath ?? deriveExtraPath([dirname9(env.nodePath), pnpmPath ? dirname9(pnpmPath) : void 0, dockerPath ? dirname9(dockerPath) : void 0]);
10130
+ const extraPath = f.extraPath ?? deriveExtraPath([dirname10(env.nodePath), pnpmPath ? dirname10(pnpmPath) : void 0, dockerPath ? dirname10(dockerPath) : void 0]);
9954
10131
  return {
9955
10132
  answers: {
9956
10133
  name: f.shipName?.trim() || env.hostname,
@@ -9963,8 +10140,8 @@ function flagWizardAnswers(f, env) {
9963
10140
  }
9964
10141
 
9965
10142
  // src/release-run.ts
9966
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync11, existsSync as existsSync19, copyFileSync, mkdirSync as mkdirSync9, mkdtempSync as mkdtempSync2, rmSync as rmSync8 } from "fs";
9967
- import { dirname as dirname10, join as join19 } from "path";
10143
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync20, copyFileSync, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, rmSync as rmSync9 } from "fs";
10144
+ import { dirname as dirname11, join as join20 } from "path";
9968
10145
  import { tmpdir as tmpdir3 } from "os";
9969
10146
 
9970
10147
  // src/release.ts
@@ -10652,13 +10829,13 @@ async function bump(o, size) {
10652
10829
  }
10653
10830
  return printed;
10654
10831
  }
10655
- const files = o.repo.release.versionFiles.map((f) => join19(o.cwd, f));
10832
+ const files = o.repo.release.versionFiles.map((f) => join20(o.cwd, f));
10656
10833
  const first = files[0];
10657
- if (!existsSync19(first)) {
10834
+ if (!existsSync20(first)) {
10658
10835
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
10659
10836
  return null;
10660
10837
  }
10661
- const current = JSON.parse(readFileSync14(first, "utf8")).version;
10838
+ const current = JSON.parse(readFileSync15(first, "utf8")).version;
10662
10839
  if (!current) {
10663
10840
  o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
10664
10841
  return null;
@@ -10671,27 +10848,27 @@ async function bump(o, size) {
10671
10848
  return version;
10672
10849
  }
10673
10850
  for (const f of files) {
10674
- const src = readFileSync14(f, "utf8");
10851
+ const src = readFileSync15(f, "utf8");
10675
10852
  const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
10676
10853
  if (out === src) {
10677
10854
  o.emit.error(`no version field in ${f}`);
10678
10855
  return null;
10679
10856
  }
10680
- writeFileSync11(f, out);
10857
+ writeFileSync12(f, out);
10681
10858
  }
10682
10859
  return version;
10683
10860
  }
10684
10861
  function writeChangelog(o, version, entries) {
10685
10862
  const file = o.repo.release.changelog;
10686
10863
  if (!file || entries.length === 0) return;
10687
- const path = join19(o.cwd, file);
10864
+ const path = join20(o.cwd, file);
10688
10865
  const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
10689
10866
  if (o.dryRun) {
10690
10867
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
10691
10868
  return;
10692
10869
  }
10693
- const existing = existsSync19(path) ? readFileSync14(path, "utf8") : "# Changelog\n";
10694
- writeFileSync11(path, insertChangelogSection(existing, section));
10870
+ const existing = existsSync20(path) ? readFileSync15(path, "utf8") : "# Changelog\n";
10871
+ writeFileSync12(path, insertChangelogSection(existing, section));
10695
10872
  }
10696
10873
  async function confirm(o, expected) {
10697
10874
  if (!o.repo.hooks.released) return void 0;
@@ -10774,7 +10951,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
10774
10951
  run2(cwd, ["worktree", "remove", "--force", dir]);
10775
10952
  } finally {
10776
10953
  try {
10777
- rmSync8(root, { recursive: true, force: true });
10954
+ rmSync9(root, { recursive: true, force: true });
10778
10955
  } finally {
10779
10956
  run2(cwd, ["worktree", "prune"]);
10780
10957
  run2(cwd, ["branch", "-D", branch]);
@@ -10784,8 +10961,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
10784
10961
  function openReleaseCheckout(o) {
10785
10962
  const { remote, base } = o.repo.branch;
10786
10963
  const branch = `crew/release-${Date.now()}`;
10787
- const root = mkdtempSync2(join19(tmpdir3(), "crew-release-"));
10788
- const dir = join19(root, "checkout");
10964
+ const root = mkdtempSync2(join20(tmpdir3(), "crew-release-"));
10965
+ const dir = join20(root, "checkout");
10789
10966
  o.emit.emit(`release checkout: ${dir}`, { step: "release" });
10790
10967
  const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
10791
10968
  try {
@@ -10795,10 +10972,10 @@ function openReleaseCheckout(o) {
10795
10972
  throw e;
10796
10973
  }
10797
10974
  for (const f of o.repo.worktrees.copy) {
10798
- const src = join19(o.cwd, f);
10799
- if (!existsSync19(src)) continue;
10800
- mkdirSync9(dirname10(join19(dir, f)), { recursive: true });
10801
- copyFileSync(src, join19(dir, f));
10975
+ const src = join20(o.cwd, f);
10976
+ if (!existsSync20(src)) continue;
10977
+ mkdirSync10(dirname11(join20(dir, f)), { recursive: true });
10978
+ copyFileSync(src, join20(dir, f));
10802
10979
  }
10803
10980
  return { dir, branch, close };
10804
10981
  }
@@ -11466,7 +11643,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
11466
11643
  }
11467
11644
 
11468
11645
  // src/watch.ts
11469
- import { watch as watchFile, existsSync as existsSync20, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
11646
+ import { watch as watchFile, existsSync as existsSync21, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
11470
11647
  var E = "\x1B[";
11471
11648
  var dim = (s) => `${E}2m${s}${E}0m`;
11472
11649
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -11567,8 +11744,8 @@ function renderHeaders(states, now = Date.now()) {
11567
11744
  return states.map((s) => renderHeader(s, now)).join("");
11568
11745
  }
11569
11746
  function readFrom(file, from) {
11570
- if (!existsSync20(file)) return { events: [], offset: from };
11571
- const size = statSync6(file).size;
11747
+ if (!existsSync21(file)) return { events: [], offset: from };
11748
+ const size = statSync7(file).size;
11572
11749
  if (size < from) return readFrom(file, 0);
11573
11750
  if (size === from) return { events: [], offset: from };
11574
11751
  const fd = openSync4(file, "r");
@@ -12518,9 +12695,54 @@ async function canMintAgentKeys(route, o = {}) {
12518
12695
  }
12519
12696
  }
12520
12697
 
12698
+ // src/capability-check.ts
12699
+ var WRITE_LEVELS = /* @__PURE__ */ new Set(["WRITE", "DELETE"]);
12700
+ async function canWriteTable(o, modelId) {
12701
+ try {
12702
+ const { access } = await get(o, `/data-models/${modelId}/access`);
12703
+ return access === void 0 ? void 0 : WRITE_LEVELS.has(access);
12704
+ } catch {
12705
+ return void 0;
12706
+ }
12707
+ }
12708
+ async function projectCapabilities(o, projectId) {
12709
+ if (!projectId) return void 0;
12710
+ try {
12711
+ return await get(o, `/projects/${projectId}/capabilities`);
12712
+ } catch {
12713
+ return void 0;
12714
+ }
12715
+ }
12716
+ async function checkCapabilities(o, found) {
12717
+ const admin = isAdminRole(found.role);
12718
+ const table2 = async (key, label, tableName2, modelId, askFor) => {
12719
+ if (!modelId) return { key, label, status: "skipped", note: `no ${tableName2} table in this project` };
12720
+ const ok = await canWriteTable(o, modelId);
12721
+ if (ok === void 0) return { key, label, status: "skipped", note: "could not read access" };
12722
+ return ok ? { key, label, status: "ok" } : { key, label, status: "missing", askFor };
12723
+ };
12724
+ const [crew, ships, repos, caps] = await Promise.all([
12725
+ table2("crew-table", "write the Crew table", "Crew", found.crewModelId ?? found.models.crew, "WRITE access on the Crew table"),
12726
+ table2("ships-table", "write the Ships table", "Ships", found.shipsModelId, "WRITE access on the Ships table"),
12727
+ table2("repos-table", "create Repos rows", "Repos", found.reposModelId, "WRITE access on the Repos table"),
12728
+ projectCapabilities(o, found.projectId)
12729
+ ]);
12730
+ const mint = admin || caps?.MINT_AGENT_KEYS ? { key: "mint-agent-keys", label: "mint agent keys", status: "ok" } : caps ? { key: "mint-agent-keys", label: "mint agent keys", status: "missing", askFor: 'the "Mint agent keys" capability' } : { key: "mint-agent-keys", label: "mint agent keys", status: "skipped", note: "could not read capabilities" };
12731
+ const sync = admin ? { key: "sync-personas", label: "sync personas and skills", status: "ok" } : { key: "sync-personas", label: "sync personas and skills", status: "missing", askFor: "the workspace admin role (personas and skills are workspace-level tables)" };
12732
+ return [crew, ships, repos, mint, sync];
12733
+ }
12734
+ function missLine(i) {
12735
+ return `${i.label} \u2014 ask a workspace admin for ${i.askFor ?? "access"}`;
12736
+ }
12737
+ function describeCapability(i) {
12738
+ if (i.status === "ok") return `ok ${i.label}`;
12739
+ if (i.status === "skipped") return `skipped ${i.label} (${i.note ?? "not checked"})`;
12740
+ return `missing ${missLine(i)}`;
12741
+ }
12742
+
12521
12743
  // src/agents-sync-run.ts
12522
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
12523
- import { dirname as dirname11 } from "path";
12744
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
12745
+ import { dirname as dirname12 } from "path";
12524
12746
  async function runAgentsSync(target, o) {
12525
12747
  const dryRun = o.dryRun ?? false;
12526
12748
  const force = o.force ?? false;
@@ -12553,12 +12775,12 @@ async function runAgentsSync(target, o) {
12553
12775
  for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
12554
12776
  if (!dryRun) {
12555
12777
  const resolvedPath = resolvedPathFor(o.stateDir, target.route);
12556
- const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
12778
+ const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
12557
12779
  raw.agentPersonas = personaResult.agentPersonas;
12558
12780
  if (skillResult) raw.agentSkills = skillResult.agentSkills;
12559
12781
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
12560
- mkdirSync10(dirname11(resolvedPath), { recursive: true });
12561
- writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
12782
+ mkdirSync11(dirname12(resolvedPath), { recursive: true });
12783
+ writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
12562
12784
  `);
12563
12785
  if (target.resolved) {
12564
12786
  target.resolved.agentPersonas = personaResult.agentPersonas;
@@ -12665,7 +12887,7 @@ import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml"
12665
12887
 
12666
12888
  // src/repos-cmd.ts
12667
12889
  import { homedir as homedir7 } from "os";
12668
- import { basename as basename7, join as join20, resolve as resolve3 } from "path";
12890
+ import { basename as basename7, join as join21, resolve as resolve3 } from "path";
12669
12891
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
12670
12892
  var ReposError = class extends Error {
12671
12893
  };
@@ -12863,7 +13085,7 @@ function planRepoClone(i) {
12863
13085
  }
12864
13086
  const ship = doc.get("ship", true);
12865
13087
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
12866
- const target = i.dir ? expandDir(i.dir, i.base) : join20(expandDir(basePath, i.base), i.route.split("/")[0], name);
13088
+ const target = i.dir ? expandDir(i.dir, i.base) : join21(expandDir(basePath, i.base), i.route.split("/")[0], name);
12867
13089
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
12868
13090
  return { ...add, remote, target };
12869
13091
  }
@@ -13006,7 +13228,7 @@ function existingRepoConfig(f) {
13006
13228
  }
13007
13229
 
13008
13230
  // src/sample-ticket.ts
13009
- import { basename as basename8, dirname as dirname13, join as join21 } from "path";
13231
+ import { basename as basename8, dirname as dirname14, join as join22 } from "path";
13010
13232
  var SAMPLE_REPO = "crew-sample";
13011
13233
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
13012
13234
  var SAMPLE_MARKER = "sample: true";
@@ -13029,8 +13251,8 @@ has something safe to work on: its test and build hooks always pass, it has no
13029
13251
  remote, and \`crew sample-ticket --remove\` deletes it.
13030
13252
  `;
13031
13253
  function sampleDir(existing, reposBase, workspace) {
13032
- if (existing.length > 0) return join21(dirname13(existing[0]), SAMPLE_REPO);
13033
- return join21(reposBase, workspace, SAMPLE_REPO);
13254
+ if (existing.length > 0) return join22(dirname14(existing[0]), SAMPLE_REPO);
13255
+ return join22(reposBase, workspace, SAMPLE_REPO);
13034
13256
  }
13035
13257
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
13036
13258
  function sampleDescription() {
@@ -13111,8 +13333,8 @@ function schedulerInstalled(plan, exists, crontab) {
13111
13333
  }
13112
13334
 
13113
13335
  // src/worktree-sweep.ts
13114
- import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "fs";
13115
- import { basename as basename9, dirname as dirname14, join as join22, resolve as resolve4 } from "path";
13336
+ import { existsSync as existsSync23, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
13337
+ import { basename as basename9, dirname as dirname15, join as join23, resolve as resolve4 } from "path";
13116
13338
  function planWorktreeSweep(target, tickets, contract) {
13117
13339
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
13118
13340
  const verified = contract.statuses.verified;
@@ -13166,12 +13388,12 @@ function worktreeTicketNumbers(target) {
13166
13388
  return [...nums];
13167
13389
  }
13168
13390
  function siblingWorktreeDirs(target) {
13169
- const parent = dirname14(target.dir.replace(/[/\\]+$/, ""));
13391
+ const parent = dirname15(target.dir.replace(/[/\\]+$/, ""));
13170
13392
  const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
13171
13393
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
13172
13394
  let names;
13173
13395
  try {
13174
- names = readdirSync5(parent);
13396
+ names = readdirSync6(parent);
13175
13397
  } catch {
13176
13398
  return [];
13177
13399
  }
@@ -13179,7 +13401,7 @@ function siblingWorktreeDirs(target) {
13179
13401
  for (const name of names) {
13180
13402
  if (!name.startsWith(stem)) continue;
13181
13403
  const m = DIR_NUMBER_SUFFIXED.exec(name);
13182
- const path = join22(parent, name);
13404
+ const path = join23(parent, name);
13183
13405
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
13184
13406
  }
13185
13407
  return out;
@@ -13214,7 +13436,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13214
13436
  let removed = 0;
13215
13437
  let keptBranches = 0;
13216
13438
  for (const a of actions) {
13217
- if (!dryRun && base && !a.keepBranch && existsSync22(a.path) && isHandBackMerge(a.path, base)) {
13439
+ if (!dryRun && base && !a.keepBranch && existsSync23(a.path) && isHandBackMerge(a.path, base)) {
13218
13440
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
13219
13441
  log.emit(
13220
13442
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -13222,7 +13444,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13222
13444
  );
13223
13445
  }
13224
13446
  }
13225
- if (!dryRun && existsSync22(a.path)) {
13447
+ if (!dryRun && existsSync23(a.path)) {
13226
13448
  let dirty = false;
13227
13449
  try {
13228
13450
  dirty = status(a.path).length > 0;
@@ -13233,7 +13455,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
13233
13455
  continue;
13234
13456
  }
13235
13457
  }
13236
- if (!dryRun && a.requirePushed && a.branch && existsSync22(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
13458
+ if (!dryRun && a.requirePushed && a.branch && existsSync23(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
13237
13459
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
13238
13460
  continue;
13239
13461
  }
@@ -13282,7 +13504,7 @@ function isHandBackMerge(path, base) {
13282
13504
  if (!msgPath) return false;
13283
13505
  let first;
13284
13506
  try {
13285
- first = readFileSync16(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13507
+ first = readFileSync17(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13286
13508
  } catch {
13287
13509
  return false;
13288
13510
  }
@@ -13387,16 +13609,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
13387
13609
  }
13388
13610
 
13389
13611
  // src/stream-sweep.ts
13390
- import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
13391
- import { join as join23 } from "path";
13612
+ import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync10, statSync as statSync8 } from "fs";
13613
+ import { join as join24 } from "path";
13392
13614
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
13393
13615
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
13394
- const dir = join23(stateDir, "streams");
13395
- if (!existsSync23(dir)) return [];
13616
+ const dir = join24(stateDir, "streams");
13617
+ if (!existsSync24(dir)) return [];
13396
13618
  const cutoff = now - retentionDays * DAY_MS2;
13397
- return readdirSync6(dir).map((name) => join23(dir, name)).filter((path) => {
13619
+ return readdirSync7(dir).map((name) => join24(dir, name)).filter((path) => {
13398
13620
  try {
13399
- return statSync7(path).mtimeMs < cutoff;
13621
+ return statSync8(path).mtimeMs < cutoff;
13400
13622
  } catch {
13401
13623
  return false;
13402
13624
  }
@@ -13410,7 +13632,7 @@ function applyStreamSweep(paths, dryRun, log) {
13410
13632
  continue;
13411
13633
  }
13412
13634
  try {
13413
- rmSync9(path, { force: true });
13635
+ rmSync10(path, { force: true });
13414
13636
  removed++;
13415
13637
  log.emit(`removed stream artifact ${path}`, { step: "worktree" });
13416
13638
  } catch (e) {
@@ -13420,11 +13642,11 @@ function applyStreamSweep(paths, dryRun, log) {
13420
13642
  return removed;
13421
13643
  }
13422
13644
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13423
- if (!existsSync23(stateDir)) return [];
13645
+ if (!existsSync24(stateDir)) return [];
13424
13646
  const cutoff = now - retentionDays * DAY_MS2;
13425
- return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join23(stateDir, name)).filter((path) => {
13647
+ return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join24(stateDir, name)).filter((path) => {
13426
13648
  try {
13427
- return statSync7(path).mtimeMs < cutoff;
13649
+ return statSync8(path).mtimeMs < cutoff;
13428
13650
  } catch {
13429
13651
  return false;
13430
13652
  }
@@ -13432,24 +13654,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13432
13654
  }
13433
13655
 
13434
13656
  // src/cli.ts
13435
- import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync15, writeFileSync as writeFileSync16, rmSync as rmSync11, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9, readdirSync as readdirSync11 } from "fs";
13657
+ import { realpathSync as realpathSync2, readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
13436
13658
  import { createInterface } from "readline/promises";
13437
13659
  import { dirname as dirOf, resolve as resolvePath } from "path";
13438
13660
 
13439
13661
  // src/ssh-keys.ts
13440
13662
  import { execFileSync as execFileSync4 } from "child_process";
13441
- import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, chmodSync } from "fs";
13442
- import { dirname as dirname15, join as join24 } from "path";
13663
+ import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, chmodSync } from "fs";
13664
+ import { dirname as dirname16, join as join25 } from "path";
13443
13665
  var SshKeyError = class extends Error {
13444
13666
  };
13445
13667
  function shipSshKeyPathFor(stateDir) {
13446
- return join24(stateDir, "ssh", "id_ed25519");
13668
+ return join25(stateDir, "ssh", "id_ed25519");
13447
13669
  }
13448
13670
  function ensureShipSshKeypair(stateDir) {
13449
13671
  const privateKeyPath = shipSshKeyPathFor(stateDir);
13450
13672
  const publicKeyPath = `${privateKeyPath}.pub`;
13451
- if (!existsSync24(privateKeyPath)) {
13452
- mkdirSync11(dirname15(privateKeyPath), { recursive: true });
13673
+ if (!existsSync25(privateKeyPath)) {
13674
+ mkdirSync12(dirname16(privateKeyPath), { recursive: true });
13453
13675
  try {
13454
13676
  execFileSync4("ssh-keygen", [
13455
13677
  "-t",
@@ -13469,10 +13691,10 @@ function ensureShipSshKeypair(stateDir) {
13469
13691
  }
13470
13692
  chmodSync(privateKeyPath, 384);
13471
13693
  }
13472
- if (!existsSync24(publicKeyPath)) {
13694
+ if (!existsSync25(publicKeyPath)) {
13473
13695
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
13474
13696
  }
13475
- const publicKey = readFileSync17(publicKeyPath, "utf8").trim();
13697
+ const publicKey = readFileSync18(publicKeyPath, "utf8").trim();
13476
13698
  return { privateKeyPath, publicKeyPath, publicKey };
13477
13699
  }
13478
13700
  function sshKeygenAvailable() {
@@ -13688,9 +13910,9 @@ function syncAllPassengerCheckouts(cfg) {
13688
13910
 
13689
13911
  // src/tunnel.ts
13690
13912
  import { spawn as spawn4 } from "child_process";
13691
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync13, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
13913
+ import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
13692
13914
  import { randomInt } from "crypto";
13693
- import { join as join25 } from "path";
13915
+ import { join as join26 } from "path";
13694
13916
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
13695
13917
  var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
13696
13918
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -13757,21 +13979,21 @@ function startTunnel(opts) {
13757
13979
  };
13758
13980
  }
13759
13981
  function tunnelStatePath(stateDir, workspaceId) {
13760
- return join25(stateDir, "passengers", `${workspaceId}.json`);
13982
+ return join26(stateDir, "passengers", `${workspaceId}.json`);
13761
13983
  }
13762
13984
  function readPersistedTunnel(stateDir, workspaceId) {
13763
13985
  const path = tunnelStatePath(stateDir, workspaceId);
13764
- if (!existsSync25(path)) return void 0;
13986
+ if (!existsSync26(path)) return void 0;
13765
13987
  try {
13766
- return JSON.parse(readFileSync18(path, "utf8"));
13988
+ return JSON.parse(readFileSync19(path, "utf8"));
13767
13989
  } catch {
13768
13990
  return void 0;
13769
13991
  }
13770
13992
  }
13771
13993
  function writePersistedTunnel(stateDir, state) {
13772
13994
  const path = tunnelStatePath(stateDir, state.workspaceId);
13773
- mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
13774
- writeFileSync13(path, `${JSON.stringify(state, null, 2)}
13995
+ mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
13996
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
13775
13997
  `);
13776
13998
  }
13777
13999
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -13868,9 +14090,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13868
14090
  }
13869
14091
  }
13870
14092
  function readAllPersistedWorkspaceIds(stateDir) {
13871
- const dir = join25(stateDir, "passengers");
13872
- if (!existsSync25(dir)) return [];
13873
- return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
14093
+ const dir = join26(stateDir, "passengers");
14094
+ if (!existsSync26(dir)) return [];
14095
+ return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
13874
14096
  }
13875
14097
 
13876
14098
  // src/mcp-endpoint-registry.ts
@@ -13990,8 +14212,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
13990
14212
  import { createServer as createHttpServer } from "http";
13991
14213
  import { spawn as spawn5 } from "child_process";
13992
14214
  import { randomBytes } from "crypto";
13993
- import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
13994
- import { join as join26 } from "path";
14215
+ import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
14216
+ import { join as join27 } from "path";
13995
14217
 
13996
14218
  // src/secret-compare.ts
13997
14219
  import { timingSafeEqual } from "crypto";
@@ -14080,21 +14302,21 @@ var defaultSpawn2 = (cmd, args, env) => {
14080
14302
  return child;
14081
14303
  };
14082
14304
  function syncDaemonStatePath(stateDir, workspaceId) {
14083
- return join26(stateDir, "passengers", `${workspaceId}-sync.json`);
14305
+ return join27(stateDir, "passengers", `${workspaceId}-sync.json`);
14084
14306
  }
14085
14307
  function readPersistedSyncDaemon(stateDir, workspaceId) {
14086
14308
  const path = syncDaemonStatePath(stateDir, workspaceId);
14087
- if (!existsSync26(path)) return void 0;
14309
+ if (!existsSync27(path)) return void 0;
14088
14310
  try {
14089
- return JSON.parse(readFileSync19(path, "utf8"));
14311
+ return JSON.parse(readFileSync20(path, "utf8"));
14090
14312
  } catch {
14091
14313
  return void 0;
14092
14314
  }
14093
14315
  }
14094
14316
  function writePersistedSyncDaemon(stateDir, state) {
14095
14317
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
14096
- mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
14097
- writeFileSync14(path, `${JSON.stringify(state, null, 2)}
14318
+ mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
14319
+ writeFileSync15(path, `${JSON.stringify(state, null, 2)}
14098
14320
  `);
14099
14321
  }
14100
14322
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -14114,10 +14336,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
14114
14336
  }
14115
14337
  }
14116
14338
  function readAllPersistedSyncWorkspaceIds(stateDir) {
14117
- const dir = join26(stateDir, "passengers");
14118
- if (!existsSync26(dir)) return [];
14339
+ const dir = join27(stateDir, "passengers");
14340
+ if (!existsSync27(dir)) return [];
14119
14341
  const suffix = "-sync.json";
14120
- return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
14342
+ return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
14121
14343
  }
14122
14344
  function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
14123
14345
  const alive = deps.isPidAlive ?? isPidAlive;
@@ -14172,12 +14394,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
14172
14394
  }
14173
14395
 
14174
14396
  // src/daemon.ts
14175
- import { existsSync as existsSync28, mkdirSync as mkdirSync14, readFileSync as readFileSync21, readdirSync as readdirSync10, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync15 } from "fs";
14176
- import { join as join28 } from "path";
14397
+ import { existsSync as existsSync29, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync16 } from "fs";
14398
+ import { join as join29 } from "path";
14177
14399
 
14178
14400
  // src/interrupted.ts
14179
- import { existsSync as existsSync27, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
14180
- import { join as join27 } from "path";
14401
+ import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
14402
+ import { join as join28 } from "path";
14181
14403
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
14182
14404
  function interruptedComment(b) {
14183
14405
  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.";
@@ -14196,21 +14418,21 @@ var pidAlive = (pid) => {
14196
14418
  };
14197
14419
  function runFinished(streamPath) {
14198
14420
  const path = `${streamPath}.meta.json`;
14199
- if (!existsSync27(path)) return false;
14421
+ if (!existsSync28(path)) return false;
14200
14422
  try {
14201
- return JSON.parse(readFileSync20(path, "utf8")).running !== true;
14423
+ return JSON.parse(readFileSync21(path, "utf8")).running !== true;
14202
14424
  } catch {
14203
14425
  return true;
14204
14426
  }
14205
14427
  }
14206
14428
  function orphanedStreams(o) {
14207
- const dir = join27(o.stateDir, "streams");
14208
- if (!existsSync27(dir)) return [];
14429
+ const dir = join28(o.stateDir, "streams");
14430
+ if (!existsSync28(dir)) return [];
14209
14431
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
14210
14432
  const out = [];
14211
- for (const name of readdirSync9(dir)) {
14433
+ for (const name of readdirSync10(dir)) {
14212
14434
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
14213
- const streamPath = join27(dir, name);
14435
+ const streamPath = join28(dir, name);
14214
14436
  if (runFinished(streamPath)) continue;
14215
14437
  const sidecar = readRunSidecar(streamPath);
14216
14438
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -14302,15 +14524,15 @@ async function runDaemonLoop(o) {
14302
14524
  function latestMtime(path) {
14303
14525
  let stat;
14304
14526
  try {
14305
- stat = statSync8(path);
14527
+ stat = statSync9(path);
14306
14528
  } catch {
14307
14529
  return 0;
14308
14530
  }
14309
14531
  if (stat.isFile()) return stat.mtimeMs;
14310
14532
  if (!stat.isDirectory()) return 0;
14311
14533
  let latest = stat.mtimeMs;
14312
- for (const entry of readdirSync10(path, { withFileTypes: true })) {
14313
- latest = Math.max(latest, latestMtime(join28(path, entry.name)));
14534
+ for (const entry of readdirSync11(path, { withFileTypes: true })) {
14535
+ latest = Math.max(latest, latestMtime(join29(path, entry.name)));
14314
14536
  }
14315
14537
  return latest;
14316
14538
  }
@@ -14319,23 +14541,23 @@ function computeTreeSignature(paths) {
14319
14541
  }
14320
14542
  function makeStaleChecker(paths, opts = {}) {
14321
14543
  const initial = computeTreeSignature(paths);
14322
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync28(opts.reloadSentinel);
14544
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync29(opts.reloadSentinel);
14323
14545
  }
14324
14546
  function reloadSentinelFor(stateDir) {
14325
- return join28(stateDir, "reload-requested");
14547
+ return join29(stateDir, "reload-requested");
14326
14548
  }
14327
14549
  function daemonStampFor(stateDir) {
14328
- return join28(stateDir, "daemon.started");
14550
+ return join29(stateDir, "daemon.started");
14329
14551
  }
14330
14552
  function noteDaemonStarted(stateDir, now = Date.now()) {
14331
- mkdirSync14(stateDir, { recursive: true });
14332
- rmSync10(reloadSentinelFor(stateDir), { force: true });
14333
- writeFileSync15(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14553
+ mkdirSync15(stateDir, { recursive: true });
14554
+ rmSync11(reloadSentinelFor(stateDir), { force: true });
14555
+ writeFileSync16(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14334
14556
  `);
14335
14557
  }
14336
14558
  function readDaemonStarted(stateDir) {
14337
14559
  try {
14338
- const v = JSON.parse(readFileSync21(daemonStampFor(stateDir), "utf8")).startedAt;
14560
+ const v = JSON.parse(readFileSync22(daemonStampFor(stateDir), "utf8")).startedAt;
14339
14561
  return typeof v === "number" ? v : null;
14340
14562
  } catch {
14341
14563
  return null;
@@ -14344,7 +14566,7 @@ function readDaemonStarted(stateDir) {
14344
14566
  function configChangedAfter(files, startedAt) {
14345
14567
  for (const f of files) {
14346
14568
  try {
14347
- if (statSync8(f).mtimeMs > startedAt) return f;
14569
+ if (statSync9(f).mtimeMs > startedAt) return f;
14348
14570
  } catch {
14349
14571
  }
14350
14572
  }
@@ -14381,6 +14603,16 @@ var SpawnBreaker = class {
14381
14603
  }
14382
14604
  };
14383
14605
  async function runOnePass(o) {
14606
+ try {
14607
+ return await runOnePassUnguarded(o);
14608
+ } catch (e) {
14609
+ if (!isUpgradeRequired(e)) throw e;
14610
+ const emit = o.newEmitter();
14611
+ await raiseFromUpgradeRequired({ state: o.state, emit, route: o.route, ship: o.ship }, e);
14612
+ return { pending: [], started: [], skipped: [] };
14613
+ }
14614
+ }
14615
+ async function runOnePassUnguarded(o) {
14384
14616
  if (o.state.isPaused()) return { pending: [], started: [], skipped: [] };
14385
14617
  await hydrateApiKeys([o.route], o.getSessionStore);
14386
14618
  const emit = o.newEmitter();
@@ -14691,7 +14923,7 @@ async function buildEnvironment(route, ship, ticket) {
14691
14923
  }
14692
14924
 
14693
14925
  // src/cli.ts
14694
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname16(process.execPath) : resolve5(dirname16(fileURLToPath(import.meta.url)), "..");
14926
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname17(process.execPath) : resolve5(dirname17(fileURLToPath(import.meta.url)), "..");
14695
14927
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
14696
14928
  var CYCLE_SECONDS2 = 120;
14697
14929
  var STALE_CYCLES = 3;
@@ -14839,6 +15071,7 @@ function usage() {
14839
15071
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
14840
15072
  crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
14841
15073
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
15074
+ crew disconnect --reset [--remove-checkouts] [--dry-run] [--json] remove everything connect and install wrote on this machine: scheduler units, Pair/MCP entries, seat keys, keychain credential, resolved state, then crew.yaml (--remove-checkouts also deletes checkouts crew cloned, never one with local changes)
14842
15075
  crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
14843
15076
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
14844
15077
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
@@ -15017,7 +15250,7 @@ if (command === "passenger-sync-daemon") {
15017
15250
  firstRun = { answers, configPath };
15018
15251
  cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
15019
15252
  } else {
15020
- cfg = loadConfig(CREW_HOME, void 0, command === "repos" ? { allowNoRoutes: true } : {});
15253
+ cfg = loadConfig(CREW_HOME, void 0, command === "repos" || command === "disconnect" && flag("reset") ? { allowNoRoutes: true } : {});
15021
15254
  }
15022
15255
  } catch (e) {
15023
15256
  if (e instanceof ConfigError) {
@@ -15048,6 +15281,7 @@ if (command === "passenger-sync-daemon") {
15048
15281
  throw e;
15049
15282
  }
15050
15283
  const state = new State(cfg.ship.stateDir);
15284
+ setCloneRecorder((dir) => recordClonedCheckout(cfg.ship.stateDir, dir));
15051
15285
  const dryRun = flag("dry-run");
15052
15286
  const force = flag("force");
15053
15287
  const EXCLUSIVE = { passengers: "passengers" };
@@ -15250,8 +15484,8 @@ if (command === "passenger-sync-daemon") {
15250
15484
  }
15251
15485
  if (host === "linux" && hasSystemd) {
15252
15486
  const label = labelFor(CREW_HOME, "run");
15253
- const staleTimer = join29(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15254
- if (existsSync29(staleTimer)) {
15487
+ const staleTimer = join30(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15488
+ if (existsSync30(staleTimer)) {
15255
15489
  process.stdout.write(
15256
15490
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
15257
15491
  `
@@ -15277,7 +15511,7 @@ if (command === "passenger-sync-daemon") {
15277
15511
  const tracker = new Tracker(c, cfg.ship);
15278
15512
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
15279
15513
  for (const r of await resolvedRepos(c)) {
15280
- if (!existsSync29(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15514
+ if (!existsSync30(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15281
15515
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
15282
15516
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
15283
15517
  }
@@ -15295,7 +15529,7 @@ if (command === "passenger-sync-daemon") {
15295
15529
  }
15296
15530
  async function doctorFix(o) {
15297
15531
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
15298
- const unitExists = (p) => existsSync29(p);
15532
+ const unitExists = (p) => existsSync30(p);
15299
15533
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
15300
15534
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
15301
15535
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -15312,7 +15546,7 @@ if (command === "passenger-sync-daemon") {
15312
15546
  for (const a of plan.actions) {
15313
15547
  if (a.kind === "enable") {
15314
15548
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
15315
- writeFileSync16(cfg.configFile, planEnable(readFileSync22(cfg.configFile, "utf8"), a.route));
15549
+ writeFileSync17(cfg.configFile, planEnable(readFileSync23(cfg.configFile, "utf8"), a.route));
15316
15550
  enabledSound.add(a.route);
15317
15551
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
15318
15552
  } else {
@@ -16311,7 +16545,7 @@ ${changed2.map((c) => ` - ${c}
16311
16545
  process.exit(1);
16312
16546
  }
16313
16547
  requireArmed("run the daemon loop");
16314
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join29(CREW_HOME, "src"), join29(CREW_HOME, "dist"), join29(CREW_HOME, "bin")];
16548
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join30(CREW_HOME, "src"), join30(CREW_HOME, "dist"), join30(CREW_HOME, "bin")];
16315
16549
  const configPaths = daemonConfigPaths(cfg);
16316
16550
  watchPaths.push(...configPaths);
16317
16551
  noteDaemonStarted(cfg.ship.stateDir);
@@ -16512,10 +16746,10 @@ ${changed2.map((c) => ` - ${c}
16512
16746
  `);
16513
16747
  if (!dryRun) {
16514
16748
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16515
- const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
16749
+ const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
16516
16750
  raw.agentSkills = result.agentSkills;
16517
- mkdirSync15(dirOf(resolvedPath), { recursive: true });
16518
- writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
16751
+ mkdirSync16(dirOf(resolvedPath), { recursive: true });
16752
+ writeFileSync17(resolvedPath, `${JSON.stringify(raw, null, 2)}
16519
16753
  `);
16520
16754
  }
16521
16755
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -16528,15 +16762,15 @@ ${changed2.map((c) => ` - ${c}
16528
16762
  process.exit(2);
16529
16763
  };
16530
16764
  if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
16531
- const configText = readFileSync22(cfg.configFile, "utf8");
16765
+ const configText = readFileSync23(cfg.configFile, "utf8");
16532
16766
  if (sub === "hooks") {
16533
16767
  try {
16534
16768
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
16535
- const dir = repoDirOf(configText, dirname16(cfg.configFile), a.route, a.name);
16536
- if (!existsSync29(dir)) throw new ReposError(`${dir} does not exist on disk`);
16537
- const files = { exists: (r) => existsSync29(resolvePath(dir, r)), read: (r) => {
16769
+ const dir = repoDirOf(configText, dirname17(cfg.configFile), a.route, a.name);
16770
+ if (!existsSync30(dir)) throw new ReposError(`${dir} does not exist on disk`);
16771
+ const files = { exists: (r) => existsSync30(resolvePath(dir, r)), read: (r) => {
16538
16772
  try {
16539
- return readFileSync22(resolvePath(dir, r), "utf8");
16773
+ return readFileSync23(resolvePath(dir, r), "utf8");
16540
16774
  } catch {
16541
16775
  return void 0;
16542
16776
  }
@@ -16548,13 +16782,13 @@ ${changed2.map((c) => ` - ${c}
16548
16782
  if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
16549
16783
  if (write === "route") {
16550
16784
  const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
16551
- writeFileSync16(cfg.configFile, plan.text);
16785
+ writeFileSync17(cfg.configFile, plan.text);
16552
16786
  written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
16553
16787
  } else if (write === "repo") {
16554
16788
  const have = existingRepoConfig(files);
16555
16789
  if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
16556
16790
  const file = resolvePath(dir, ".crew.yaml");
16557
- writeFileSync16(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16791
+ writeFileSync17(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
16558
16792
  written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
16559
16793
  }
16560
16794
  if (flag("json")) {
@@ -16591,7 +16825,7 @@ ${changed2.map((c) => ` - ${c}
16591
16825
  if (sub === "list" && !routeName) usage2();
16592
16826
  if (sub === "add") {
16593
16827
  try {
16594
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync29(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16828
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync30(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16595
16829
  routeName = a.route;
16596
16830
  addPath = a.path;
16597
16831
  addName = a.name;
@@ -16620,15 +16854,15 @@ ${changed2.map((c) => ` - ${c}
16620
16854
  }
16621
16855
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
16622
16856
  let resolvedRemotes;
16623
- if (existsSync29(resolvedFile)) {
16857
+ if (existsSync30(resolvedFile)) {
16624
16858
  try {
16625
- resolvedRemotes = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16859
+ resolvedRemotes = JSON.parse(readFileSync23(resolvedFile, "utf8"));
16626
16860
  } catch {
16627
16861
  }
16628
16862
  }
16629
16863
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
16630
16864
  const text = configText;
16631
- const base = dirname16(cfg.configFile);
16865
+ const base = dirname17(cfg.configFile);
16632
16866
  try {
16633
16867
  if (sub === "list") {
16634
16868
  const repos = configuredRepos(text, routeName, base);
@@ -16637,7 +16871,7 @@ ${changed2.map((c) => ` - ${c}
16637
16871
  for (const r of repos) {
16638
16872
  const origin2 = originOf(r.dir);
16639
16873
  const row = matchRepoName(origin2, resolvedRemotes);
16640
- const note = !existsSync29(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16874
+ const note = !existsSync30(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16641
16875
  process.stdout.write(`${r.name} ${r.dir} ${note}
16642
16876
  `);
16643
16877
  }
@@ -16649,9 +16883,9 @@ ${changed2.map((c) => ` - ${c}
16649
16883
  ` : `${human}
16650
16884
  `);
16651
16885
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
16652
- if (existsSync29(plan2.target) && (existsSync29(`${plan2.target}/.git`) || readdirSync11(plan2.target).length > 0)) {
16886
+ if (existsSync30(plan2.target) && (existsSync30(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
16653
16887
  throw new ReposError(
16654
- existsSync29(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
16888
+ existsSync30(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
16655
16889
  );
16656
16890
  }
16657
16891
  if (dryRun) {
@@ -16669,12 +16903,12 @@ ${changed2.map((c) => ` - ${c}
16669
16903
  throw new ReposError(`${e.message}${authHint}
16670
16904
  crew.yaml was not changed`);
16671
16905
  }
16672
- writeFileSync16(cfg.configFile, plan2.text);
16906
+ writeFileSync17(cfg.configFile, plan2.text);
16673
16907
  say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
16674
16908
  break;
16675
16909
  }
16676
16910
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
16677
- const top = existsSync29(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16911
+ const top = existsSync30(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16678
16912
  if (!top) throw new ReposError(`${target} is not a git checkout`);
16679
16913
  const origin = originOf(top);
16680
16914
  const plan = planRepoAdd({
@@ -16723,15 +16957,15 @@ ${changed2.map((c) => ` - ${c}
16723
16957
  const created = await rowTracker.createRepoRow(rowPayload);
16724
16958
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
16725
16959
  `);
16726
- if (existsSync29(resolvedFile)) {
16727
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16960
+ if (existsSync30(resolvedFile)) {
16961
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
16728
16962
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
16729
16963
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
16730
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
16964
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
16731
16965
  `);
16732
16966
  }
16733
16967
  }
16734
- writeFileSync16(cfg.configFile, plan.text);
16968
+ writeFileSync17(cfg.configFile, plan.text);
16735
16969
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
16736
16970
  `);
16737
16971
  } catch (e) {
@@ -17016,6 +17250,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17016
17250
  }
17017
17251
  const route2 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
17018
17252
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, route2);
17253
+ const capabilities = await checkCapabilities(authOpts, found);
17254
+ if (flag("json")) {
17255
+ process.stdout.write(`${JSON.stringify({ event: "capabilities", route: route2, items: capabilities })}
17256
+ `);
17257
+ } else {
17258
+ process.stderr.write("capabilities for this key:\n");
17259
+ for (const i of capabilities) process.stderr.write(` ${describeCapability(i)}
17260
+ `);
17261
+ }
17262
+ const capabilityMisses = capabilities.filter((i) => i.status === "missing").map(missLine);
17019
17263
  if (found.locksModelId && !dryRun) {
17020
17264
  const configured = cfg.routes.find((r) => r.route === route2);
17021
17265
  const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
@@ -17041,10 +17285,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17041
17285
  }
17042
17286
  }
17043
17287
  let previous;
17044
- const hadPreviousFile = existsSync29(resolvedPath);
17288
+ const hadPreviousFile = existsSync30(resolvedPath);
17045
17289
  if (hadPreviousFile) {
17046
17290
  try {
17047
- previous = JSON.parse(readFileSync22(resolvedPath, "utf8"));
17291
+ previous = JSON.parse(readFileSync23(resolvedPath, "utf8"));
17048
17292
  } catch {
17049
17293
  }
17050
17294
  }
@@ -17134,6 +17378,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17134
17378
  holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
17135
17379
  ...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
17136
17380
  ...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
17381
+ ...capabilityMisses.length > 0 ? { capabilityMisses } : {},
17137
17382
  ...operator ? { operator } : {},
17138
17383
  ...contract ? { contract } : {},
17139
17384
  ...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
@@ -17152,10 +17397,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17152
17397
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
17153
17398
  `);
17154
17399
  } else {
17155
- mkdirSync15(dirname16(resolvedPath), { recursive: true });
17400
+ mkdirSync16(dirname17(resolvedPath), { recursive: true });
17156
17401
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
17157
17402
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
17158
- writeFileSync16(tmpPath, `${JSON.stringify(resolved, null, 2)}
17403
+ writeFileSync17(tmpPath, `${JSON.stringify(resolved, null, 2)}
17159
17404
  `);
17160
17405
  renameSync2(tmpPath, resolvedPath);
17161
17406
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -17190,8 +17435,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17190
17435
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
17191
17436
  `);
17192
17437
  } else {
17193
- mkdirSync15(dirname16(apiKeyFile), { recursive: true });
17194
- writeFileSync16(apiKeyFile, `${apiKeyVar}=${apiKey}
17438
+ mkdirSync16(dirname17(apiKeyFile), { recursive: true });
17439
+ writeFileSync17(apiKeyFile, `${apiKeyVar}=${apiKey}
17195
17440
  `, { mode: 384 });
17196
17441
  process.stderr.write(`API key written to ${apiKeyFile}
17197
17442
  `);
@@ -17200,7 +17445,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17200
17445
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
17201
17446
  const wizard = firstRun;
17202
17447
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
17203
- const routeDir = value("dir") ?? (writeFirstRun ? join29(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17448
+ const routeDir = value("dir") ?? (writeFirstRun ? join30(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17204
17449
  const routeBlock = renderConnection(
17205
17450
  found,
17206
17451
  route2,
@@ -17795,10 +18040,76 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17795
18040
  case "disconnect": {
17796
18041
  const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
17797
18042
  const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
17798
- for (const h of disconnectHarnesses) {
17799
- process.stderr.write(`${pairInstallerFor(h).removePair({ url, bin: cfg.ship.agent.bin, dryRun })}
18043
+ const removePairEntries = (mcpUrl, dry) => [...disconnectHarnesses].map((h) => pairInstallerFor(h).removePair({ url: mcpUrl, bin: cfg.ship.agent.bin, dryRun: dry }));
18044
+ if (!flag("reset")) {
18045
+ for (const line of removePairEntries(url, dryRun)) process.stderr.write(`${line}
17800
18046
  `);
18047
+ break;
17801
18048
  }
18049
+ const host = hostPlatform();
18050
+ let resetStore;
18051
+ try {
18052
+ resetStore = getSessionStore3();
18053
+ } catch {
18054
+ resetStore = void 0;
18055
+ }
18056
+ const effects = {
18057
+ async uninstallUnits(dry) {
18058
+ const lines = [];
18059
+ if (host === "windows") return lines;
18060
+ const log = { emit: (m) => {
18061
+ lines.push(m);
18062
+ }, warn: (m) => {
18063
+ lines.push(m);
18064
+ } };
18065
+ for (const job of ["run", "release", "passengers"]) {
18066
+ await applyUninstall(planUninstall(cfg.ship, CREW_HOME, host, detectSystemd(), job), CREW_HOME, dry, log);
18067
+ }
18068
+ return lines;
18069
+ },
18070
+ removePair: removePairEntries,
18071
+ async revokeSeatKeys(r, dry) {
18072
+ const outcomes = await revokeSeatKeys(r, { userAgent: cfg.ship.userAgent, dryRun: dry });
18073
+ return outcomes.map((o) => ({ line: describeRevokeOutcome(o), ok: o.action !== "failed" }));
18074
+ },
18075
+ async credential(r) {
18076
+ if (!resetStore) return { stored: false, clear: async () => {
18077
+ } };
18078
+ const store = resetStore;
18079
+ const h = hostFromUrl3(r.baseUrl);
18080
+ const [slug] = splitRoute(r.route);
18081
+ const session = await store.get(h, slug);
18082
+ return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
18083
+ },
18084
+ inspectCheckout(dir) {
18085
+ const dirty = status(dir).map((c) => c.path);
18086
+ const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync2(p) !== realpathSync2(dir));
18087
+ return { dirty, linkedWorktrees };
18088
+ }
18089
+ };
18090
+ const droppedRoutes = [];
18091
+ try {
18092
+ const rawCfg = parseYaml2(readFileSync23(cfg.configFile, "utf8"));
18093
+ for (const r of rawCfg?.routes ?? []) {
18094
+ if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
18095
+ try {
18096
+ splitRoute(r.route);
18097
+ } catch {
18098
+ continue;
18099
+ }
18100
+ droppedRoutes.push({ route: r.route, baseUrl: typeof r.baseUrl === "string" ? r.baseUrl : rawCfg?.ship?.baseUrl ?? DEFAULT_BASE_URL });
18101
+ }
18102
+ } catch {
18103
+ }
18104
+ const steps = await runReset(
18105
+ { routes: cfg.routes, droppedRoutes, stateDir: cfg.ship.stateDir, configFile: cfg.configFile, dryRun, removeCheckouts: flag("remove-checkouts") },
18106
+ effects
18107
+ );
18108
+ if (flag("json")) process.stdout.write(`${JSON.stringify({ dryRun, ok: !resetIncomplete(steps), steps })}
18109
+ `);
18110
+ else for (const s of steps) process.stdout.write(`${describeResetStep(s)}
18111
+ `);
18112
+ if (resetIncomplete(steps)) process.exitCode = 1;
17802
18113
  break;
17803
18114
  }
17804
18115
  case "install": {
@@ -17820,8 +18131,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17820
18131
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
17821
18132
  break;
17822
18133
  }
17823
- mkdirSync15(cfg.ship.stateDir, { recursive: true });
17824
- writeFileSync16(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
18134
+ mkdirSync16(cfg.ship.stateDir, { recursive: true });
18135
+ writeFileSync17(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
17825
18136
  `);
17826
18137
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
17827
18138
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -17872,7 +18183,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17872
18183
  }
17873
18184
  case "log": {
17874
18185
  try {
17875
- const text = readFileSync22(cfg.ship.logFile, "utf8").split("\n");
18186
+ const text = readFileSync23(cfg.ship.logFile, "utf8").split("\n");
17876
18187
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
17877
18188
  `);
17878
18189
  } catch {
@@ -17895,8 +18206,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17895
18206
  };
17896
18207
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
17897
18208
  const tracker = new Tracker(route, cfg.ship);
17898
- const configText = readFileSync22(cfg.configFile, "utf8");
17899
- const cfgBase = dirname16(cfg.configFile);
18209
+ const configText = readFileSync23(cfg.configFile, "utf8");
18210
+ const cfgBase = dirname17(cfg.configFile);
17900
18211
  const configured = configuredRepos(configText, route.route, cfgBase);
17901
18212
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
17902
18213
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -17924,7 +18235,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17924
18235
  }
17925
18236
  if (configured.some((r) => r.dir === dir)) {
17926
18237
  try {
17927
- writeFileSync16(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
18238
+ writeFileSync17(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
17928
18239
  } catch (e) {
17929
18240
  if (e instanceof ReposError) fail(e.message);
17930
18241
  throw e;
@@ -17932,29 +18243,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17932
18243
  }
17933
18244
  if (row) {
17934
18245
  await tracker.deleteRepoRow(row.id);
17935
- if (existsSync29(resolvedFile)) {
17936
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
18246
+ if (existsSync30(resolvedFile)) {
18247
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
17937
18248
  if (raw.repoNames) delete raw.repoNames[row.id];
17938
18249
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
17939
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
18250
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
17940
18251
  `);
17941
18252
  }
17942
18253
  }
17943
- if (existsSync29(join29(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync11(dir, { recursive: true, force: true });
18254
+ if (existsSync30(join30(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
17944
18255
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
17945
18256
  break;
17946
18257
  }
17947
18258
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
17948
- const fresh = !existsSync29(join29(dir, ".git"));
17949
- if (existsSync29(dir) && fresh && readdirSync11(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
18259
+ const fresh = !existsSync30(join30(dir, ".git"));
18260
+ if (existsSync30(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
17950
18261
  if (dryRun) {
17951
18262
  say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
17952
18263
  break;
17953
18264
  }
17954
18265
  if (fresh) {
17955
- mkdirSync15(dir, { recursive: true });
17956
- writeFileSync16(join29(dir, "README.md"), SAMPLE_README);
17957
- writeFileSync16(join29(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
18266
+ mkdirSync16(dir, { recursive: true });
18267
+ writeFileSync17(join30(dir, "README.md"), SAMPLE_README);
18268
+ writeFileSync17(join30(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
17958
18269
  const g = (...a) => {
17959
18270
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
17960
18271
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -17972,17 +18283,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17972
18283
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
17973
18284
  });
17974
18285
  repoId = (await tracker.createRepoRow(payload)).id;
17975
- if (existsSync29(resolvedFile)) {
17976
- const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
18286
+ if (existsSync30(resolvedFile)) {
18287
+ const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
17977
18288
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
17978
18289
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
17979
- writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
18290
+ writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
17980
18291
  `);
17981
18292
  }
17982
18293
  }
17983
18294
  if (!configured.some((r) => r.dir === dir)) {
17984
18295
  try {
17985
- writeFileSync16(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
18296
+ writeFileSync17(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
17986
18297
  } catch (e) {
17987
18298
  if (e instanceof ReposError) fail(e.message);
17988
18299
  throw e;
@@ -18084,6 +18395,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18084
18395
  const issueLabelById = new Map(engagedTickets.map((t) => [t.id, t.issue_id]));
18085
18396
  const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route.resolved?.operator ?? "")).catch(() => []);
18086
18397
  const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
18398
+ const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
18087
18399
  if (flag("json")) {
18088
18400
  const repos = await resolvedRepos(route);
18089
18401
  const seatIds = route.resolved?.seats ?? {};
@@ -18143,6 +18455,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
18143
18455
  watermark: state.watermark(),
18144
18456
  route: {
18145
18457
  name: route.route,
18458
+ // Additive (CREW-2248): the running version and the workspace's floor. `version` stays 1.
18459
+ crewVersion: sentVersion(cfg.ship),
18460
+ crewMinVersion,
18146
18461
  // Additive (CREW-1496): each project and its work mode. `version` stays 1.
18147
18462
  projects: projects.map((p) => ({ id: p.id, name: p.name, workMode: p.mode })),
18148
18463
  dir: route.dir,
@@ -18272,7 +18587,7 @@ watermark: ${state.watermark()}
18272
18587
  }
18273
18588
  for (const r of repos) {
18274
18589
  const remote = r.config.branch.remote;
18275
- if (existsSync29(r.dir)) {
18590
+ if (existsSync30(r.dir)) {
18276
18591
  pushAuth.push({
18277
18592
  route: c.route,
18278
18593
  repo: r.name,
@@ -18300,7 +18615,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18300
18615
  );
18301
18616
  try {
18302
18617
  const evFile = eventFileFor(cfg.ship.stateDir);
18303
- const evSize = existsSync29(evFile) ? statSync9(evFile).size : 0;
18618
+ const evSize = existsSync30(evFile) ? statSync10(evFile).size : 0;
18304
18619
  process.stdout.write(
18305
18620
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
18306
18621
  ` : "events log: ok\n"
@@ -18311,7 +18626,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18311
18626
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
18312
18627
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
18313
18628
  try {
18314
- return readFileSync22(p, "utf8");
18629
+ return readFileSync23(p, "utf8");
18315
18630
  } catch {
18316
18631
  return null;
18317
18632
  }
@@ -18327,12 +18642,25 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18327
18642
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
18328
18643
  if (changed2) {
18329
18644
  process.stdout.write(
18330
- `daemon config: WARN ${changed2} changed at ${new Date(statSync9(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
18645
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync10(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
18331
18646
  `
18332
18647
  );
18333
18648
  }
18334
18649
  }
18335
18650
  const tracker = new Tracker(route, cfg.ship);
18651
+ {
18652
+ const have = sentVersion(cfg.ship);
18653
+ const floor = await readCrewFloor(route, cfg.ship);
18654
+ if (have && floor) {
18655
+ process.stdout.write(
18656
+ `crew version: ${belowFloor(have, floor) ? "WARN " : ""}${have} (workspace requires ${floor})${belowFloor(have, floor) ? " \u2014 update crew (the Crew app, or `npm install -g @tablation/crew`)" : ""}
18657
+ `
18658
+ );
18659
+ } else if (have) {
18660
+ process.stdout.write(`crew version: ${have}${floor === null ? " (workspace sets no minimum)" : ""}
18661
+ `);
18662
+ }
18663
+ }
18336
18664
  const contractProblems = validateContract(tracker.contract);
18337
18665
  process.stdout.write(
18338
18666
  contractProblems.length ? `contract: ${contractProblems.length} problem(s)
@@ -18387,6 +18715,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
18387
18715
  const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
18388
18716
  const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
18389
18717
  process.stdout.write(`api key role: ${keyRole ?? "unknown"}
18718
+ `);
18719
+ for (const m of route.resolved?.capabilityMisses ?? []) process.stdout.write(`capability: MISSING ${m} (re-run \`crew connect\` once granted)
18390
18720
  `);
18391
18721
  const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
18392
18722
  process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}