@tablation/crew 0.91.0 → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1219 -786
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -352,8 +352,8 @@ var Emitter = class _Emitter {
352
352
  * actually knows which route it's working (release, per-repo poll) uses
353
353
  * this instead, so its events aren't all attributed to `routes[0]`.
354
354
  */
355
- forRoute(route) {
356
- return new _Emitter({ ...this.opts, route, cycleId: this.cycle });
355
+ forRoute(route2) {
356
+ return new _Emitter({ ...this.opts, route: route2, cycleId: this.cycle });
357
357
  }
358
358
  /** Times a step and emits its outcome, so a view can show duration. */
359
359
  async timed(step, message, fn, role) {
@@ -363,11 +363,11 @@ var Emitter = class _Emitter {
363
363
  const result = await fn();
364
364
  this.emit(message, { data: { ms: this.now().getTime() - started, ok: true } });
365
365
  return result;
366
- } catch (err) {
367
- this.error(`${message} \u2014 ${err.message}`, {
366
+ } catch (err2) {
367
+ this.error(`${message} \u2014 ${err2.message}`, {
368
368
  data: { ms: this.now().getTime() - started, ok: false }
369
369
  });
370
- throw err;
370
+ throw err2;
371
371
  }
372
372
  }
373
373
  };
@@ -1521,8 +1521,8 @@ function shipToolchains(ship) {
1521
1521
  };
1522
1522
  return [legacy, ...explicit];
1523
1523
  }
1524
- function resolveToolchain(ship, route, repo) {
1525
- const name = (repo && route?.repoOverrides?.[repo]?.toolchain) ?? route?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
1524
+ function resolveToolchain(ship, route2, repo) {
1525
+ const name = (repo && route2?.repoOverrides?.[repo]?.toolchain) ?? route2?.toolchain ?? ship.toolchain ?? LEGACY_TOOLCHAIN;
1526
1526
  const found = shipToolchains(ship).find((t) => t.name === name);
1527
1527
  if (!found) throw new Error(`toolchain "${name}" is not defined (have: ${shipToolchains(ship).map((t) => t.name).join(", ")})`);
1528
1528
  return found;
@@ -1537,12 +1537,12 @@ function toolchainProvider(spec) {
1537
1537
  if (isOmlxLauncher(spec)) return "local";
1538
1538
  return adapterFor(spec.harness).provider(spec);
1539
1539
  }
1540
- function resolveSeat(ship, route, role, persona = {}, repo) {
1540
+ function resolveSeat(ship, route2, role, persona = {}, repo) {
1541
1541
  const warnings = [];
1542
- const base = resolveToolchain(ship, route, repo);
1542
+ const base = resolveToolchain(ship, route2, repo);
1543
1543
  const baseVendor = toolchainVendor(base);
1544
- const repoToolchain = repo ? route?.repoOverrides?.[repo]?.toolchain : void 0;
1545
- const baseWhy = repoToolchain ? `repo ${repo} toolchain ${base.name}` : route?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
1544
+ const repoToolchain = repo ? route2?.repoOverrides?.[repo]?.toolchain : void 0;
1545
+ const baseWhy = repoToolchain ? `repo ${repo} toolchain ${base.name}` : route2?.toolchain ? `route toolchain ${base.name}` : ship.toolchain ? `ship default ${base.name}` : `default ${base.name}`;
1546
1546
  let chosen = base;
1547
1547
  let toolchainWhy = baseWhy;
1548
1548
  if (persona.vendor && persona.vendor !== baseVendor) {
@@ -2626,23 +2626,23 @@ function parseBranchOverride(raw, where, file) {
2626
2626
  if (base === void 0 && name === void 0 && push === void 0 && remote === void 0 && lowercase === void 0) return void 0;
2627
2627
  return { base, name, push, remote, ...lowercase !== void 0 ? { lowercase } : {} };
2628
2628
  }
2629
- function mergeRouteRelease(route, override) {
2630
- const provider = override?.ci?.provider ?? route.ci?.provider;
2631
- const ref = override?.ci?.ref ?? route.ci?.ref;
2632
- const match = override?.verify?.match ?? route.verify?.match;
2633
- const timeoutSeconds = override?.verify?.timeoutSeconds ?? route.verify?.timeoutSeconds;
2634
- const intervalSeconds = override?.verify?.intervalSeconds ?? route.verify?.intervalSeconds;
2635
- const isolateMax = override?.isolateFailures?.maxCandidates ?? route.isolateFailures?.maxCandidates;
2629
+ function mergeRouteRelease(route2, override) {
2630
+ const provider = override?.ci?.provider ?? route2.ci?.provider;
2631
+ const ref = override?.ci?.ref ?? route2.ci?.ref;
2632
+ const match = override?.verify?.match ?? route2.verify?.match;
2633
+ const timeoutSeconds = override?.verify?.timeoutSeconds ?? route2.verify?.timeoutSeconds;
2634
+ const intervalSeconds = override?.verify?.intervalSeconds ?? route2.verify?.intervalSeconds;
2635
+ const isolateMax = override?.isolateFailures?.maxCandidates ?? route2.isolateFailures?.maxCandidates;
2636
2636
  return {
2637
- mode: override?.mode ?? route.mode,
2637
+ mode: override?.mode ?? route2.mode,
2638
2638
  ci: provider !== void 0 || ref !== void 0 ? { provider, ref } : void 0,
2639
2639
  verify: match !== void 0 || timeoutSeconds !== void 0 || intervalSeconds !== void 0 ? { match, timeoutSeconds, intervalSeconds } : void 0,
2640
2640
  ...isolateMax === void 0 ? {} : { isolateFailures: { maxCandidates: isolateMax } },
2641
- versioning: override?.versioning ?? route.versioning,
2642
- versionFiles: override?.versionFiles ?? route.versionFiles,
2643
- changelog: override?.changelog ?? route.changelog,
2644
- tag: override?.tag ?? route.tag,
2645
- tagPattern: override?.tagPattern ?? route.tagPattern
2641
+ versioning: override?.versioning ?? route2.versioning,
2642
+ versionFiles: override?.versionFiles ?? route2.versionFiles,
2643
+ changelog: override?.changelog ?? route2.changelog,
2644
+ tag: override?.tag ?? route2.tag,
2645
+ tagPattern: override?.tagPattern ?? route2.tagPattern
2646
2646
  };
2647
2647
  }
2648
2648
  function expand(p, base) {
@@ -2695,30 +2695,30 @@ function configSearchPath(crewHome) {
2695
2695
  ];
2696
2696
  }
2697
2697
  function findConfigFile(crewHome) {
2698
- const candidates = configSearchPath(crewHome);
2699
- return candidates.find((c) => existsSync9(c)) ?? candidates[candidates.length - 1];
2698
+ const candidates2 = configSearchPath(crewHome);
2699
+ return candidates2.find((c) => existsSync9(c)) ?? candidates2[candidates2.length - 1];
2700
2700
  }
2701
2701
  var defaultConfigPath = () => join11(process.env.XDG_CONFIG_HOME ?? join11(homedir6(), ".config"), "crew", "crew.yaml");
2702
- function routeSlug(route) {
2703
- return route.replace(/\//g, "-");
2702
+ function routeSlug(route2) {
2703
+ return route2.replace(/\//g, "-");
2704
2704
  }
2705
- function resolvedPathFor(stateDir, route) {
2706
- const [workspace, project] = splitRoute(route);
2705
+ function resolvedPathFor(stateDir, route2) {
2706
+ const [workspace, project] = splitRoute(route2);
2707
2707
  return join11(stateDir, "resolved", workspace, `${project}.json`);
2708
2708
  }
2709
- function apiKeyPathFor(stateDir, route) {
2710
- const [workspace, project] = splitRoute(route);
2709
+ function apiKeyPathFor(stateDir, route2) {
2710
+ const [workspace, project] = splitRoute(route2);
2711
2711
  return join11(stateDir, "keys", workspace, `${project}.env`);
2712
2712
  }
2713
- function splitRoute(route) {
2714
- const parts = route.split("/");
2713
+ function splitRoute(route2) {
2714
+ const parts = route2.split("/");
2715
2715
  if (parts.length !== 2 || !parts[0] || !parts[1]) {
2716
- throw new ConfigError(`route must be "workspace/project" (got "${route}")`);
2716
+ throw new ConfigError(`route must be "workspace/project" (got "${route2}")`);
2717
2717
  }
2718
2718
  return [parts[0], parts[1]];
2719
2719
  }
2720
- function readResolvedFile(stateDir, route) {
2721
- const path = resolvedPathFor(stateDir, route);
2720
+ function readResolvedFile(stateDir, route2) {
2721
+ const path = resolvedPathFor(stateDir, route2);
2722
2722
  if (!existsSync9(path)) return void 0;
2723
2723
  try {
2724
2724
  return JSON.parse(readFileSync10(path, "utf8"));
@@ -3019,14 +3019,14 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
3019
3019
  if (toolchain !== void 0 && !toolchainNames.has(toolchain)) {
3020
3020
  missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
3021
3021
  }
3022
- const route = missing.req(c?.route, `${where}.route`);
3022
+ const route2 = missing.req(c?.route, `${where}.route`);
3023
3023
  if (c?.hostPeriscopes === true && c?.hostPassengers !== true) {
3024
- missing.add(`${where} (route "${route}") sets hostPeriscopes: true without hostPassengers: true \u2014 a Passenger must be able to board before they can open a periscope`);
3024
+ missing.add(`${where} (route "${route2}") sets hostPeriscopes: true without hostPassengers: true \u2014 a Passenger must be able to board before they can open a periscope`);
3025
3025
  }
3026
- const routeParts = route ? route.split("/") : [];
3026
+ const routeParts = route2 ? route2.split("/") : [];
3027
3027
  const routeWellFormed = routeParts.length === 2 && routeParts.every(Boolean);
3028
- if (route && !routeWellFormed) {
3029
- missing.add(`${where}.route must be "workspace/project" (got "${route}")`);
3028
+ if (route2 && !routeWellFormed) {
3029
+ missing.add(`${where}.route must be "workspace/project" (got "${route2}")`);
3030
3030
  }
3031
3031
  const repoDirs = {};
3032
3032
  const repoOverrides = {};
@@ -3063,13 +3063,13 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
3063
3063
  const hasRepos = Object.keys(repoDirs).length > 0;
3064
3064
  const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
3065
3065
  const dirIsCloneBase = !c?.dir && !hasRepos && !Array.isArray(c?.repos);
3066
- const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join11(reposBasePath, splitRoute(route)[0]) : "";
3066
+ const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join11(reposBasePath, splitRoute(route2)[0]) : "";
3067
3067
  const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
3068
3068
  if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
3069
3069
  missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
3070
3070
  }
3071
3071
  const result = {
3072
- route,
3072
+ route: route2,
3073
3073
  enabled: c?.enabled === true,
3074
3074
  area: c?.area,
3075
3075
  dir,
@@ -3100,7 +3100,7 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
3100
3100
  hostPassengers: c?.hostPassengers === true,
3101
3101
  hostPeriscopes: c?.hostPeriscopes === true
3102
3102
  };
3103
- const resolvedFile = routeWellFormed ? readResolvedFile(stateDir, route) : void 0;
3103
+ const resolvedFile = routeWellFormed ? readResolvedFile(stateDir, route2) : void 0;
3104
3104
  result.resolved = parseResolved(resolvedFile ?? c?.resolved, missing, where);
3105
3105
  result.contract = resolvedFile?.contract ?? c?.contract ?? null;
3106
3106
  missing.throwIfAny(file);
@@ -3115,9 +3115,9 @@ function locateDir(cfg, dir, commonDirOf) {
3115
3115
  return target2 === b || target2.startsWith(`${b}/`);
3116
3116
  };
3117
3117
  const find = (target2) => {
3118
- for (const route of cfg.routes) {
3119
- const repo = reposOf(route).find((x) => within(x.dir, target2));
3120
- if (repo) return { route, repo, checkout: resolve(repo.dir) };
3118
+ for (const route2 of cfg.routes) {
3119
+ const repo = reposOf(route2).find((x) => within(x.dir, target2));
3120
+ if (repo) return { route: route2, repo, checkout: resolve(repo.dir) };
3121
3121
  }
3122
3122
  return void 0;
3123
3123
  };
@@ -3127,8 +3127,8 @@ function locateDir(cfg, dir, commonDirOf) {
3127
3127
  const common = commonDirOf?.(target);
3128
3128
  return common ? find(dirname4(resolve(target, common))) : void 0;
3129
3129
  }
3130
- function findRoute(cfg, route) {
3131
- if (!route) {
3130
+ function findRoute(cfg, route2) {
3131
+ if (!route2) {
3132
3132
  const enabled = cfg.routes.filter((r) => r.enabled);
3133
3133
  if (enabled.length === 1) return enabled[0];
3134
3134
  if (cfg.routes.length === 1) return cfg.routes[0];
@@ -3136,9 +3136,9 @@ function findRoute(cfg, route) {
3136
3136
  `this ship has ${cfg.routes.length} routes \u2014 name one: ${cfg.routes.map((r) => r.route).join(", ")}`
3137
3137
  );
3138
3138
  }
3139
- const found = cfg.routes.find((r) => r.route === route);
3139
+ const found = cfg.routes.find((r) => r.route === route2);
3140
3140
  if (!found) {
3141
- throw new ConfigError(`no route "${route}" (have: ${cfg.routes.map((r) => r.route).join(", ")})`);
3141
+ throw new ConfigError(`no route "${route2}" (have: ${cfg.routes.map((r) => r.route).join(", ")})`);
3142
3142
  }
3143
3143
  return found;
3144
3144
  }
@@ -3184,13 +3184,13 @@ var KEYCHAIN_ELIGIBLE_ROUTES = /* @__PURE__ */ new WeakSet();
3184
3184
  async function hydrateApiKeys(routes, getStore = getSessionStore) {
3185
3185
  let store;
3186
3186
  let storeUnavailable = false;
3187
- for (const route of routes) {
3188
- if (!KEYCHAIN_ELIGIBLE_ROUTES.has(route)) {
3187
+ for (const route2 of routes) {
3188
+ if (!KEYCHAIN_ELIGIBLE_ROUTES.has(route2)) {
3189
3189
  try {
3190
- resolveApiKey(route);
3190
+ resolveApiKey(route2);
3191
3191
  continue;
3192
3192
  } catch {
3193
- KEYCHAIN_ELIGIBLE_ROUTES.add(route);
3193
+ KEYCHAIN_ELIGIBLE_ROUTES.add(route2);
3194
3194
  }
3195
3195
  }
3196
3196
  if (storeUnavailable) continue;
@@ -3204,13 +3204,13 @@ async function hydrateApiKeys(routes, getStore = getSessionStore) {
3204
3204
  }
3205
3205
  let workspaceSlug;
3206
3206
  try {
3207
- [workspaceSlug] = splitRoute(route.route);
3207
+ [workspaceSlug] = splitRoute(route2.route);
3208
3208
  } catch {
3209
3209
  continue;
3210
3210
  }
3211
3211
  try {
3212
- const session = await store.get(hostFromUrl(route.baseUrl), workspaceSlug);
3213
- if (session?.apiKey) route.apiKey = session.apiKey;
3212
+ const session = await store.get(hostFromUrl(route2.baseUrl), workspaceSlug);
3213
+ if (session?.apiKey) route2.apiKey = session.apiKey;
3214
3214
  } catch {
3215
3215
  }
3216
3216
  }
@@ -3404,10 +3404,10 @@ function runScript(script, opts) {
3404
3404
  child.stdout.on("data", absorb);
3405
3405
  child.stderr.on("data", absorb);
3406
3406
  const timer = opts.timeoutMs ? setTimeout(() => child.kill("SIGKILL"), opts.timeoutMs) : void 0;
3407
- child.on("error", (err) => {
3407
+ child.on("error", (err2) => {
3408
3408
  if (timer) clearTimeout(timer);
3409
3409
  resolvePromise({ code: 127, output: `${output}
3410
- crew: cannot run ${shell.bin}: ${err.message}` });
3410
+ crew: cannot run ${shell.bin}: ${err2.message}` });
3411
3411
  });
3412
3412
  child.on("close", (code) => {
3413
3413
  if (timer) clearTimeout(timer);
@@ -3418,8 +3418,8 @@ crew: cannot run ${shell.bin}: ${err.message}` });
3418
3418
  }
3419
3419
 
3420
3420
  // src/notify.ts
3421
- async function notify(route, ship, n, emit, dryRun = false) {
3422
- const script = route.hooks.notify;
3421
+ async function notify(route2, ship, n, emit, dryRun = false) {
3422
+ const script = route2.hooks.notify;
3423
3423
  if (!script) return false;
3424
3424
  if (dryRun) {
3425
3425
  emit.emit(`would notify: [${n.level}] ${n.headline}`);
@@ -3427,12 +3427,12 @@ async function notify(route, ship, n, emit, dryRun = false) {
3427
3427
  }
3428
3428
  try {
3429
3429
  const r = await runScript(script, {
3430
- cwd: route.dir,
3430
+ cwd: route2.dir,
3431
3431
  env: {
3432
3432
  CREW_LEVEL: n.level,
3433
3433
  CREW_HEADLINE: n.headline,
3434
3434
  CREW_DETAIL: n.detail ?? "",
3435
- CREW_ROUTE: route.route,
3435
+ CREW_ROUTE: route2.route,
3436
3436
  CREW_SHIP: ship.name
3437
3437
  },
3438
3438
  shell: resolveShell(ship.shell),
@@ -3457,18 +3457,18 @@ The \`${f.hook}\` hook (\`${f.label}\`) printed, last lines:
3457
3457
  ${f.tail}
3458
3458
  \`\`\``;
3459
3459
  }
3460
- function describeRelease(o, route) {
3460
+ function describeRelease(o, route2) {
3461
3461
  if (o.integrated) {
3462
3462
  return {
3463
3463
  level: "ok",
3464
- headline: `${route}: integrated ${o.version ?? o.merged.length + " ticket(s)"}`,
3464
+ headline: `${route2}: integrated ${o.version ?? o.merged.length + " ticket(s)"}`,
3465
3465
  detail: "merged to the base branch \u2014 this repo has no deploy step"
3466
3466
  };
3467
3467
  }
3468
3468
  if (o.deployed) {
3469
3469
  return {
3470
3470
  level: o.confirmed === false ? "warn" : "ok",
3471
- headline: `${route}: released ${o.version ?? "a new version"}`,
3471
+ headline: `${route2}: released ${o.version ?? "a new version"}`,
3472
3472
  detail: o.confirmed === false ? "deploy ran, but the target did not confirm the new commit in time" : `${o.merged.length} ticket(s) shipped`
3473
3473
  };
3474
3474
  }
@@ -3476,7 +3476,7 @@ function describeRelease(o, route) {
3476
3476
  if (o.stopped === "tests failed") {
3477
3477
  return {
3478
3478
  level: "fail",
3479
- headline: `${route}: release blocked \u2014 tests failed`,
3479
+ headline: `${route2}: release blocked \u2014 tests failed`,
3480
3480
  detail: withHookTail(
3481
3481
  (o.composition ? `the target stays on the previous release
3482
3482
 
@@ -3491,7 +3491,7 @@ ${o.isolation}` : ""),
3491
3491
  if (o.stopped === "setup failed") {
3492
3492
  return {
3493
3493
  level: "fail",
3494
- headline: `${route}: release blocked \u2014 setup failed`,
3494
+ headline: `${route2}: release blocked \u2014 setup failed`,
3495
3495
  detail: withHookTail(
3496
3496
  "the setup hook failed in the release checkout; the target stays on the previous release",
3497
3497
  o.hookFailure
@@ -3501,7 +3501,7 @@ ${o.isolation}` : ""),
3501
3501
  if (o.stopped === "deploy failed") {
3502
3502
  return {
3503
3503
  level: "fail",
3504
- headline: `${route}: deploy FAILED`,
3504
+ headline: `${route2}: deploy FAILED`,
3505
3505
  detail: withHookTail(
3506
3506
  "the version is merged to the base branch but not live \u2014 if the deploy target was the problem, run `crew deploy` to retry (do not re-release); if the merged code itself failed to build, fix forward on a ticket branch, and merging it clears the not-retrying guard",
3507
3507
  o.hookFailure
@@ -3511,7 +3511,7 @@ ${o.isolation}` : ""),
3511
3511
  if (o.stopped === "build failed") {
3512
3512
  return {
3513
3513
  level: "fail",
3514
- headline: `${route}: build FAILED`,
3514
+ headline: `${route2}: build FAILED`,
3515
3515
  detail: withHookTail(`the target stays on the previous release \u2014 nothing was deployed${o.isolation ? `
3516
3516
 
3517
3517
  ${o.isolation}` : ""}`, o.hookFailure)
@@ -3520,25 +3520,25 @@ ${o.isolation}` : ""}`, o.hookFailure)
3520
3520
  if (o.blockedCycles && o.blockedCycles >= 2) {
3521
3521
  return {
3522
3522
  level: "fail",
3523
- headline: `${route}: release blocked \u2014 ${o.blockKind === "branch" ? "checkout is not on the base branch" : "working tree is not clean"}`,
3523
+ headline: `${route2}: release blocked \u2014 ${o.blockKind === "branch" ? "checkout is not on the base branch" : "working tree is not clean"}`,
3524
3524
  detail: `${o.stopped ?? "blocked"} (blocked ${o.blockedCycles} cycles running) \u2014 a person has to clear it`
3525
3525
  };
3526
3526
  }
3527
3527
  return null;
3528
3528
  }
3529
- function recoveredFailureHeadlines(o, route) {
3530
- const setup = `${route}: release blocked \u2014 setup failed`;
3531
- const tests = `${route}: release blocked \u2014 tests failed`;
3532
- const build = `${route}: build FAILED`;
3533
- const deploy = `${route}: deploy FAILED`;
3529
+ function recoveredFailureHeadlines(o, route2) {
3530
+ const setup = `${route2}: release blocked \u2014 setup failed`;
3531
+ const tests = `${route2}: release blocked \u2014 tests failed`;
3532
+ const build = `${route2}: build FAILED`;
3533
+ const deploy = `${route2}: deploy FAILED`;
3534
3534
  if (o.deployed || o.integrated) {
3535
3535
  return [
3536
3536
  setup,
3537
3537
  tests,
3538
3538
  build,
3539
3539
  deploy,
3540
- `${route}: release blocked \u2014 checkout is not on the base branch`,
3541
- `${route}: release blocked \u2014 working tree is not clean`
3540
+ `${route2}: release blocked \u2014 checkout is not on the base branch`,
3541
+ `${route2}: release blocked \u2014 working tree is not clean`
3542
3542
  ];
3543
3543
  }
3544
3544
  switch (o.stopped) {
@@ -3604,8 +3604,8 @@ function pathFor(ship) {
3604
3604
  return parts.join(":");
3605
3605
  }
3606
3606
  function findOnPath(bin, searchPath) {
3607
- const candidates = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join12(dir, bin));
3608
- for (const candidate of candidates) {
3607
+ const candidates2 = bin.includes("/") ? [bin] : searchPath.split(":").filter(Boolean).map((dir) => join12(dir, bin));
3608
+ for (const candidate of candidates2) {
3609
3609
  try {
3610
3610
  accessSync(candidate, constants.X_OK);
3611
3611
  if (statSync3(candidate).isFile()) return candidate;
@@ -4103,18 +4103,18 @@ function gitRaw(cwd, args) {
4103
4103
  try {
4104
4104
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4105
4105
  } catch (e) {
4106
- const err = e;
4107
- const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
4108
- throw new GitError(`git ${args.join(" ")}: ${why || err.message.trim()}`);
4106
+ const err2 = e;
4107
+ const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4108
+ throw new GitError(`git ${args.join(" ")}: ${why || err2.message.trim()}`);
4109
4109
  }
4110
4110
  }
4111
4111
  function git(cwd, args) {
4112
4112
  try {
4113
4113
  return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
4114
4114
  } catch (e) {
4115
- const err = e;
4116
- const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
4117
- throw new GitError(`git ${args.join(" ")}: ${why || err.message.trim()}`);
4115
+ const err2 = e;
4116
+ const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4117
+ throw new GitError(`git ${args.join(" ")}: ${why || err2.message.trim()}`);
4118
4118
  }
4119
4119
  }
4120
4120
  function gitOk(cwd, args) {
@@ -4153,19 +4153,19 @@ function branchForIssue(cwd, issueKey, templates = { name: "issue-{number}" }, r
4153
4153
  function branchCandidates(issueKey, templates, render2) {
4154
4154
  const num = issueKey.replace(/^\D+/, "");
4155
4155
  const unpadded = String(Number.parseInt(num, 10));
4156
- const candidates = [];
4156
+ const candidates2 = [];
4157
4157
  for (const template of [templates.name, templates.push].filter(Boolean)) {
4158
4158
  const exact = render2(template);
4159
- candidates.push(exact);
4160
- if (num !== unpadded) candidates.push(exact.replace(num, unpadded));
4159
+ candidates2.push(exact);
4160
+ if (num !== unpadded) candidates2.push(exact.replace(num, unpadded));
4161
4161
  if (template.includes("{slug}")) {
4162
4162
  const prefix = render2(template.slice(0, template.indexOf("{slug}")));
4163
- if (prefix) candidates.push(`${prefix}*`);
4163
+ if (prefix) candidates2.push(`${prefix}*`);
4164
4164
  }
4165
4165
  }
4166
- candidates.push(`issue-${num}`);
4167
- if (num !== unpadded) candidates.push(`issue-${unpadded}`);
4168
- return [...new Set(candidates)];
4166
+ candidates2.push(`issue-${num}`);
4167
+ if (num !== unpadded) candidates2.push(`issue-${unpadded}`);
4168
+ return [...new Set(candidates2)];
4169
4169
  }
4170
4170
  function remoteBranchForIssue(cwd, remote, issueKey, templates = { name: "issue-{number}" }, render2 = (t) => t.replace(/\{number\}/g, issueKey.replace(/^\D+/, ""))) {
4171
4171
  const prefix = `refs/remotes/${remote}/`;
@@ -4303,11 +4303,11 @@ function findSquashByBranchTip(cwd, key, branch, range) {
4303
4303
  function findSquashOfBranch(cwd, key, branch, base, range) {
4304
4304
  const byTip = findSquashByBranchTip(cwd, key, branch, range);
4305
4305
  if (byTip) return byTip;
4306
- const candidates = keyCommitsInRange(cwd, key, range);
4307
- if (candidates.length === 0) return null;
4306
+ const candidates2 = keyCommitsInRange(cwd, key, range);
4307
+ if (candidates2.length === 0) return null;
4308
4308
  const mb = gitOk(cwd, ["merge-base", base, branch]);
4309
4309
  const branchPatch = mb ? patchIdOf(cwd, mb, branch) : null;
4310
- for (const sha of candidates) {
4310
+ for (const sha of candidates2) {
4311
4311
  if (gitOk(cwd, ["diff", "--quiet", branch, sha]) !== null) return sha;
4312
4312
  if (branchPatch && gitOk(cwd, ["rev-parse", "--verify", "-q", `${sha}^`]) !== null && patchIdOf(cwd, `${sha}^`, sha) === branchPatch) return sha;
4313
4313
  }
@@ -4556,9 +4556,9 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
4556
4556
  try {
4557
4557
  execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
4558
4558
  } catch (e) {
4559
- const err = e;
4560
- const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
4561
- throw new GitError(`git clone ${url} ${dir}: ${why || err.message.trim()}`);
4559
+ const err2 = e;
4560
+ const why = [err2.stderr, err2.stdout].map(streamText).filter(Boolean).join(" / ");
4561
+ throw new GitError(`git clone ${url} ${dir}: ${why || err2.message.trim()}`);
4562
4562
  }
4563
4563
  cloneRecorder?.(dir);
4564
4564
  return true;
@@ -4591,8 +4591,8 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4591
4591
  }
4592
4592
  };
4593
4593
  } catch (e) {
4594
- const err = e;
4595
- if (err.code !== "EEXIST") throw err;
4594
+ const err2 = e;
4595
+ if (err2.code !== "EEXIST") throw err2;
4596
4596
  try {
4597
4597
  if (Date.now() - statSync4(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
4598
4598
  try {
@@ -4689,13 +4689,13 @@ function baseAttention(repo, outcome, remote, base, nowIso = (/* @__PURE__ */ ne
4689
4689
  if (outcome.action === "level" || outcome.action === "fast-forwarded") return { clear: baseUnsafeKey(repo) };
4690
4690
  return null;
4691
4691
  }
4692
- var agentLogRefusedKey = (route) => `agent_log_refused:${route}`;
4693
- function agentLogRefusedItem(route, role, status2, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
4692
+ var agentLogRefusedKey = (route2) => `agent_log_refused:${route2}`;
4693
+ function agentLogRefusedItem(route2, role, status2, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
4694
4694
  return {
4695
4695
  kind: "agent_log_refused",
4696
- key: agentLogRefusedKey(route),
4696
+ key: agentLogRefusedKey(route2),
4697
4697
  since: nowIso,
4698
- message: `${route}: the tracker refused the ${role} seat's run report (${status2}) \u2014 runs and ticket spend are not being recorded; check the seat key's role`
4698
+ message: `${route2}: the tracker refused the ${role} seat's run report (${status2}) \u2014 runs and ticket spend are not being recorded; check the seat key's role`
4699
4699
  };
4700
4700
  }
4701
4701
  var diskLowKey = (repo) => `disk_low:${repo}`;
@@ -4737,25 +4737,25 @@ function parseHookMissing(key, routeName) {
4737
4737
  if (slash <= 0) return null;
4738
4738
  return { repo: rest.slice(0, slash), hook: rest.slice(slash + 1) };
4739
4739
  }
4740
- function repoStopFor(items, route) {
4740
+ function repoStopFor(items, route2) {
4741
4741
  const stops = /* @__PURE__ */ new Map();
4742
4742
  for (const i of items) {
4743
4743
  let repo = null;
4744
4744
  if (i.kind === "base_unsafe") repo = i.key.slice("base_unsafe:".length);
4745
4745
  else if (i.kind === "disk_low") repo = i.key.slice("disk_low:".length);
4746
4746
  else if (i.kind === "hook_missing") {
4747
- const h = parseHookMissing(i.key, route.route);
4747
+ const h = parseHookMissing(i.key, route2.route);
4748
4748
  if (h?.hook === "setup") repo = h.repo;
4749
4749
  }
4750
4750
  if (repo && !stops.has(repo)) stops.set(repo, i.message);
4751
4751
  }
4752
4752
  if (stops.size === 0) return () => null;
4753
4753
  return (t) => {
4754
- const name = repoTargetFor(route, t.repo_id)?.name;
4754
+ const name = repoTargetFor(route2, t.repo_id)?.name;
4755
4755
  return name ? stops.get(name) ?? null : null;
4756
4756
  };
4757
4757
  }
4758
- function recheckEnvironmentStops(state, route, ship, deps = {}) {
4758
+ function recheckEnvironmentStops(state, route2, ship, deps = {}) {
4759
4759
  const onPath = deps.onPath ?? findOnPath;
4760
4760
  const dockerProblem = deps.dockerProblem ?? dockerPathProblem;
4761
4761
  const cleared = [];
@@ -4768,8 +4768,8 @@ function recheckEnvironmentStops(state, route, ship, deps = {}) {
4768
4768
  continue;
4769
4769
  }
4770
4770
  if (item.kind !== "hook_missing") continue;
4771
- const h = parseHookMissing(item.key, route.route);
4772
- const target = h && reposOf(route).find((t) => t.name === h.repo);
4771
+ const h = parseHookMissing(item.key, route2.route);
4772
+ const target = h && reposOf(route2).find((t) => t.name === h.repo);
4773
4773
  if (!h || !target) continue;
4774
4774
  let cfg;
4775
4775
  try {
@@ -4786,11 +4786,11 @@ function recheckEnvironmentStops(state, route, ship, deps = {}) {
4786
4786
  }
4787
4787
  return cleared;
4788
4788
  }
4789
- function recheckBaseStops(state, route) {
4789
+ function recheckBaseStops(state, route2) {
4790
4790
  const open = state.shipAttention().previous().filter((i) => i.kind === "base_unsafe");
4791
4791
  const cleared = [];
4792
4792
  for (const item of open) {
4793
- const target = reposOf(route).find((t) => baseUnsafeKey(t.name) === item.key);
4793
+ const target = reposOf(route2).find((t) => baseUnsafeKey(t.name) === item.key);
4794
4794
  if (!target) continue;
4795
4795
  const cfg = resolveRepoConfig(loadRepoConfig(target.dir), void 0, target.dir);
4796
4796
  const outcome = refreshBaseBranch(target.dir, cfg.branch.remote, cfg.branch.base, true);
@@ -4975,8 +4975,8 @@ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
4975
4975
  run2(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
4976
4976
  return { state: "ok" };
4977
4977
  } catch (e) {
4978
- const err = e;
4979
- const raw = (err.stderr ? String(err.stderr) : err.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
4978
+ const err2 = e;
4979
+ const raw = (err2.stderr ? String(err2.stderr) : err2.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
4980
4980
  const detail = raw || "push dry-run failed";
4981
4981
  const f = o.forge;
4982
4982
  if (!f) return { state: "failed", detail, hint: pushAuthHint(o.remoteUrl) };
@@ -5350,8 +5350,8 @@ var State = class _State {
5350
5350
  * release blocked in one repo counted cycles for all of them — a dirty tree
5351
5351
  * in one checkout would have silently held up an unrelated project.
5352
5352
  */
5353
- release(route) {
5354
- const suffix = _State.safe(route);
5353
+ release(route2) {
5354
+ const suffix = _State.safe(route2);
5355
5355
  const blocked = `.release-blocked-${suffix}`;
5356
5356
  const failed = `.deploy-failed-sha-${suffix}`;
5357
5357
  const gateFailed = `.test-gate-failed-${suffix}`;
@@ -5438,8 +5438,8 @@ var State = class _State {
5438
5438
  * cycle, so "how long has this been waiting" only exists if it survives
5439
5439
  * between them.
5440
5440
  */
5441
- fairness(route) {
5442
- const suffix = _State.safe(route);
5441
+ fairness(route2) {
5442
+ const suffix = _State.safe(route2);
5443
5443
  const streakFile = `.fairness-streak-${suffix}`;
5444
5444
  const ticketFile = `.fairness-ticket-${suffix}`;
5445
5445
  const sinceFile = `.fairness-since-${suffix}`;
@@ -5506,8 +5506,8 @@ var State = class _State {
5506
5506
  * currently needing a person emits once, rather than being silently
5507
5507
  * skipped as though already notified.
5508
5508
  */
5509
- attention(route) {
5510
- const file = `.attention-${_State.safe(route)}.json`;
5509
+ attention(route2) {
5510
+ const file = `.attention-${_State.safe(route2)}.json`;
5511
5511
  return {
5512
5512
  previous: () => {
5513
5513
  try {
@@ -5529,8 +5529,8 @@ var State = class _State {
5529
5529
  * caller persists the new set beside `attention(route).persist(...)`. A
5530
5530
  * missing or corrupt file reads as empty, so a fresh install warns once.
5531
5531
  */
5532
- unroutable(route) {
5533
- const file = `.unroutable-${_State.safe(route)}.json`;
5532
+ unroutable(route2) {
5533
+ const file = `.unroutable-${_State.safe(route2)}.json`;
5534
5534
  return {
5535
5535
  previous: () => {
5536
5536
  try {
@@ -5709,11 +5709,11 @@ function repoSection(r, key, heading) {
5709
5709
  return lines;
5710
5710
  }
5711
5711
  function renderEnvironment(i) {
5712
- const { route, contract } = i;
5712
+ const { route: route2, contract } = i;
5713
5713
  const repos = i.repos;
5714
5714
  const only = repos.length === 1 ? repos[0] : void 0;
5715
- const models = route.resolved?.models;
5716
- const epicsModelId = route.resolved?.epicsModelId;
5715
+ const models = route2.resolved?.models;
5716
+ const epicsModelId = route2.resolved?.epicsModelId;
5717
5717
  const mediaLibraryModelId = i.attachments?.mediaLibraryModelId ?? null;
5718
5718
  const key = "ISSUE-000";
5719
5719
  const lines = [
@@ -5725,7 +5725,7 @@ function renderEnvironment(i) {
5725
5725
  "",
5726
5726
  "## The board",
5727
5727
  "",
5728
- `- Base URL: \`${route.baseUrl}\``,
5728
+ `- Base URL: \`${route2.baseUrl}\``,
5729
5729
  `- Your key is in the \`${API_KEY_VAR}\` environment variable. Send it as`,
5730
5730
  ` \`Authorization: Bearer $${API_KEY_VAR}\`. **Do not read it out of any file**,`,
5731
5731
  " and never write it into a comment, a commit or a log line.",
@@ -5743,9 +5743,9 @@ function renderEnvironment(i) {
5743
5743
  ...mediaLibraryModelId ? [`- Media Library: \`${mediaLibraryModelId}\` (what a ticket's \`${contract.columns.attachments}\` ids point into)`] : []
5744
5744
  );
5745
5745
  }
5746
- const workspaceId = route.resolved?.workspaceId;
5746
+ const workspaceId = route2.resolved?.workspaceId;
5747
5747
  if (workspaceId) {
5748
- const base = route.baseUrl.replace(/\/+$/, "");
5748
+ const base = route2.baseUrl.replace(/\/+$/, "");
5749
5749
  lines.push(
5750
5750
  "",
5751
5751
  `- Workspace id: \`${workspaceId}\``,
@@ -5801,7 +5801,7 @@ function renderEnvironment(i) {
5801
5801
  "another (the digest prints other ships by name) \u2014 write its name, never a Ships row id."
5802
5802
  );
5803
5803
  }
5804
- const seatIds = Object.entries(route.resolved?.seats ?? {}).filter(([, id]) => !!id);
5804
+ const seatIds = Object.entries(route2.resolved?.seats ?? {}).filter(([, id]) => !!id);
5805
5805
  if (seatIds.length > 0) {
5806
5806
  lines.push(
5807
5807
  "",
@@ -5854,7 +5854,7 @@ function renderEnvironment(i) {
5854
5854
  "",
5855
5855
  `\`${contract.statuses.verified}\` and the closed statuses are **never yours to set**.`
5856
5856
  );
5857
- const areaId = route.resolved?.areaId;
5857
+ const areaId = route2.resolved?.areaId;
5858
5858
  if (areaId || repos.some((r) => r.id)) {
5859
5859
  lines.push(
5860
5860
  "",
@@ -6249,7 +6249,7 @@ function queueAgentRun(stateDir, name, q) {
6249
6249
  mkdirSync9(queueDir(stateDir), { recursive: true });
6250
6250
  writeFileSync10(join15(queueDir(stateDir), `${name}.json`), JSON.stringify(q));
6251
6251
  }
6252
- async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
6252
+ async function replayQueuedAgentRuns(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
6253
6253
  const dir = queueDir(stateDir);
6254
6254
  if (!existsSync14(dir)) return 0;
6255
6255
  let sent = 0;
@@ -6261,7 +6261,7 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
6261
6261
  } catch {
6262
6262
  continue;
6263
6263
  }
6264
- if (q.version !== 1 || q.route !== route || now.toISOString() < q.nextAttemptAt) continue;
6264
+ if (q.version !== 1 || q.route !== route2 || now.toISOString() < q.nextAttemptAt) continue;
6265
6265
  try {
6266
6266
  const apiKey = await keyFor(q.role);
6267
6267
  if (!apiKey) continue;
@@ -6302,7 +6302,7 @@ function queueTicketCounters(stateDir, name, q) {
6302
6302
  mkdirSync9(countersDir(stateDir), { recursive: true });
6303
6303
  writeFileSync10(join15(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
6304
6304
  }
6305
- async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
6305
+ async function replayQueuedTicketCounters(stateDir, route2, keyFor, log, now = /* @__PURE__ */ new Date()) {
6306
6306
  const dir = countersDir(stateDir);
6307
6307
  if (!existsSync14(dir)) return 0;
6308
6308
  let sent = 0;
@@ -6314,7 +6314,7 @@ async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /*
6314
6314
  } catch {
6315
6315
  continue;
6316
6316
  }
6317
- if (q.version !== 1 || q.route !== route || now.toISOString() < q.nextAttemptAt) continue;
6317
+ if (q.version !== 1 || q.route !== route2 || now.toISOString() < q.nextAttemptAt) continue;
6318
6318
  try {
6319
6319
  const apiKey = await keyFor(q.role);
6320
6320
  if (!apiKey) continue;
@@ -6594,8 +6594,8 @@ function projectModeLabels(rows) {
6594
6594
 
6595
6595
  // src/board-lock.ts
6596
6596
  import { StaleWriteError } from "@tablation/client";
6597
- function releaseLockScope(route, repo) {
6598
- return `${route}/${repo}`;
6597
+ function releaseLockScope(route2, repo) {
6598
+ return `${route2}/${repo}`;
6599
6599
  }
6600
6600
  async function acquireBoardLock(client2, modelId, scope, holderLabel, ttlMs, log = {}) {
6601
6601
  if (!modelId) return { ok: true, release: async () => {
@@ -6723,21 +6723,21 @@ var Tracker = class {
6723
6723
  */
6724
6724
  /** Told once per failed best-effort read (`events`); poll.ts points it at the pass's emitter. */
6725
6725
  onReadFailure;
6726
- constructor(route, ship, opts = {}) {
6727
- this.route = route;
6728
- if (!route.resolved) {
6726
+ constructor(route2, ship, opts = {}) {
6727
+ this.route = route2;
6728
+ if (!route2.resolved) {
6729
6729
  throw new ConfigError(
6730
- `route "${route.route}" has no resolved ids \u2014 run \`crew connect\` to discover them from the project`
6730
+ `route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` to discover them from the project`
6731
6731
  );
6732
6732
  }
6733
- this.models = route.resolved.models;
6734
- this.contract = resolveContract(route.contract);
6733
+ this.models = route2.resolved.models;
6734
+ this.contract = resolveContract(route2.contract);
6735
6735
  this.userAgent = ship.userAgent;
6736
6736
  this.shipName = ship.name;
6737
6737
  this.stateDir = ship.stateDir;
6738
6738
  this.client = new TablationClient({
6739
- baseUrl: `${route.baseUrl}/api`,
6740
- apiKey: opts.apiKey ?? resolveApiKey(route),
6739
+ baseUrl: `${route2.baseUrl}/api`,
6740
+ apiKey: opts.apiKey ?? resolveApiKey(route2),
6741
6741
  // Cloudflare 403s (error 1010) default agents on this host.
6742
6742
  headers: { "User-Agent": ship.userAgent }
6743
6743
  });
@@ -7205,8 +7205,8 @@ var Tracker = class {
7205
7205
  if (res.status === 404) return [];
7206
7206
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
7207
7207
  rows = await res.json();
7208
- } catch (err) {
7209
- this.onReadFailure?.(`reading ${type} events failed: ${err instanceof Error ? err.message : String(err)}`);
7208
+ } catch (err2) {
7209
+ this.onReadFailure?.(`reading ${type} events failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
7210
7210
  return [];
7211
7211
  }
7212
7212
  return rows.filter((e) => e.event_type === type).sort((a, b) => Date.parse(b.occurred_at ?? b.created_at ?? "") - Date.parse(a.occurred_at ?? a.created_at ?? ""));
@@ -8059,14 +8059,14 @@ async function enforceHandoff(i) {
8059
8059
  return { kind: "failed", why: e.message };
8060
8060
  }
8061
8061
  }
8062
- async function guardRunHandoff(route, ship, tracker, role, workingId, log, since2) {
8063
- const replayed = await replayRoutePending(route, ship, tracker, log, role);
8062
+ async function guardRunHandoff(route2, ship, tracker, role, workingId, log, since2) {
8063
+ const replayed = await replayRoutePending(route2, ship, tracker, log, role);
8064
8064
  if (!workingId || role !== "dev" && role !== "design" && role !== "qa") return null;
8065
- const seatId = route.resolved?.seats[role];
8065
+ const seatId = route2.resolved?.seats[role];
8066
8066
  if (!seatId) return null;
8067
8067
  try {
8068
8068
  const t = await tracker.ticket(workingId);
8069
- const resolved = repoConfigFor(route, t);
8069
+ const resolved = repoConfigFor(route2, t);
8070
8070
  if (!resolved) return null;
8071
8071
  const { target, cfg } = resolved;
8072
8072
  if (role === "qa") {
@@ -8094,7 +8094,7 @@ async function guardRunHandoff(route, ship, tracker, role, workingId, log, since
8094
8094
  log.emit(`${t.issue_id}: dropped ${branch} \u2014 the run made no commits beyond ${cfg.branch.base}`, { ticket: t.issue_id, step: "handoff" });
8095
8095
  }
8096
8096
  }
8097
- if (since2) await guardRunStrays(route, ship, tracker, role, workingId, since2, log);
8097
+ if (since2) await guardRunStrays(route2, ship, tracker, role, workingId, since2, log);
8098
8098
  return outcome;
8099
8099
  } catch (e) {
8100
8100
  log.warn(`could not check the hand-off: ${e.message}`, { step: "handoff" });
@@ -8117,29 +8117,29 @@ async function retireRunWorktree(tracker, dir, cfg, t, role, seatId, shipName2,
8117
8117
  log.warn(`could not retire the run's worktree: ${e.message}`, { step: "handoff" });
8118
8118
  }
8119
8119
  }
8120
- function repoConfigFor(route, t) {
8121
- const target = repoTargetFor(route, t.repo_id);
8120
+ function repoConfigFor(route2, t) {
8121
+ const target = repoTargetFor(route2, t.repo_id);
8122
8122
  if (!target) return null;
8123
- const o = route.repoOverrides[target.name];
8123
+ const o = route2.repoOverrides[target.name];
8124
8124
  const cfg = resolveRepoConfig(loadRepoConfig(target.dir), {
8125
- hooks: { ...route.hooks, ...o?.hooks },
8126
- labels: { ...route.labels, ...o?.labels },
8127
- branch: { ...route.branch, ...o?.branch },
8125
+ hooks: { ...route2.hooks, ...o?.hooks },
8126
+ labels: { ...route2.labels, ...o?.labels },
8127
+ branch: { ...route2.branch, ...o?.branch },
8128
8128
  worktrees: { nameOverride: o?.worktreeName }
8129
8129
  }, target.dir);
8130
8130
  return { target, cfg };
8131
8131
  }
8132
- async function replayRoutePending(route, ship, tracker, log, role) {
8132
+ async function replayRoutePending(route2, ship, tracker, log, role) {
8133
8133
  try {
8134
8134
  return await replayPendingHandoffs({
8135
8135
  writer: tracker,
8136
8136
  contract: tracker.contract,
8137
8137
  stateDir: ship.stateDir,
8138
- route: routeSlug(route.route),
8138
+ route: routeSlug(route2.route),
8139
8139
  role,
8140
- seats: route.resolved?.seats ?? {},
8140
+ seats: route2.resolved?.seats ?? {},
8141
8141
  checkout: (t) => {
8142
- const r = repoConfigFor(route, t);
8142
+ const r = repoConfigFor(route2, t);
8143
8143
  return r ? { dir: r.target.dir, cfg: r.cfg } : null;
8144
8144
  },
8145
8145
  log
@@ -8195,23 +8195,23 @@ ${i.shipName ? `${i.shipName}'s ` : "A "}${i.role} run set it to \`${st.handoff}
8195
8195
  }
8196
8196
  }
8197
8197
  }
8198
- async function guardRunStrays(route, ship, tracker, role, workingId, since2, log) {
8199
- const seatId = route.resolved?.seats[role];
8198
+ async function guardRunStrays(route2, ship, tracker, role, workingId, since2, log) {
8199
+ const seatId = route2.resolved?.seats[role];
8200
8200
  if (!seatId) return;
8201
8201
  try {
8202
8202
  const affinity = await tracker.claimAffinity();
8203
- const candidates = await tracker.handedOffSince(since2, workingId);
8204
- if (candidates.length === 0) return;
8203
+ const candidates2 = await tracker.handedOffSince(since2, workingId);
8204
+ if (candidates2.length === 0) return;
8205
8205
  await guardStrayHandoffs({
8206
8206
  writer: tracker,
8207
8207
  contract: tracker.contract,
8208
- candidates,
8208
+ candidates: candidates2,
8209
8209
  seatId,
8210
8210
  role,
8211
8211
  myShipId: affinity?.shipId ?? null,
8212
8212
  shipName: ship.name,
8213
8213
  checkout: (t) => {
8214
- const r = repoConfigFor(route, t);
8214
+ const r = repoConfigFor(route2, t);
8215
8215
  return r ? { dir: r.target.dir, cfg: r.cfg } : null;
8216
8216
  },
8217
8217
  log
@@ -8230,14 +8230,14 @@ function writePendingHandoff(stateDir, rec) {
8230
8230
  mkdirSync10(dir, { recursive: true });
8231
8231
  writeFileSync11(join18(dir, `${rec.ticketRecordId}.json`), JSON.stringify(rec, null, 2));
8232
8232
  }
8233
- function listPendingHandoffs(stateDir, route) {
8233
+ function listPendingHandoffs(stateDir, route2) {
8234
8234
  const dir = pendingDirFor(stateDir);
8235
8235
  if (!existsSync17(dir)) return [];
8236
8236
  const out = [];
8237
8237
  for (const f of readdirSync7(dir).filter((n) => n.endsWith(".json")).sort()) {
8238
8238
  try {
8239
8239
  const rec = JSON.parse(readFileSync15(join18(dir, f), "utf8"));
8240
- if (rec.version === 1 && rec.ticketRecordId && (!route || rec.route === route)) out.push(rec);
8240
+ if (rec.version === 1 && rec.ticketRecordId && (!route2 || rec.route === route2)) out.push(rec);
8241
8241
  } catch {
8242
8242
  }
8243
8243
  }
@@ -8393,61 +8393,61 @@ function personaDefaultPrompt(promptsDir, role) {
8393
8393
  if (!existsSync18(commonPath)) return readFileSync16(briefPath, "utf8");
8394
8394
  return `${readFileSync16(commonPath, "utf8")}${readFileSync16(briefPath, "utf8")}`;
8395
8395
  }
8396
- async function resolveAgentId(route, role, opts) {
8397
- const cached = route.resolved?.agentPersonas?.[role]?.agentId;
8396
+ async function resolveAgentId(route2, role, opts) {
8397
+ const cached = route2.resolved?.agentPersonas?.[role]?.agentId;
8398
8398
  if (cached) return cached;
8399
- const seatId = route.resolved?.seats?.[role];
8400
- if (!seatId || !route.resolved) return void 0;
8399
+ const seatId = route2.resolved?.seats?.[role];
8400
+ if (!seatId || !route2.resolved) return void 0;
8401
8401
  const client2 = new TablationClient2({
8402
- baseUrl: `${route.baseUrl}/api`,
8403
- apiKey: resolveApiKey(route),
8402
+ baseUrl: `${route2.baseUrl}/api`,
8403
+ apiKey: resolveApiKey(route2),
8404
8404
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
8405
8405
  });
8406
8406
  try {
8407
- const row = await client2.records.get(route.resolved.models.crew, seatId);
8407
+ const row = await client2.records.get(route2.resolved.models.crew, seatId);
8408
8408
  return row.agent_id ?? void 0;
8409
8409
  } catch {
8410
8410
  return void 0;
8411
8411
  }
8412
8412
  }
8413
- async function fetchPersonaPrompt(route, role, opts) {
8414
- const agentId = await resolveAgentId(route, role, opts);
8415
- if (!agentId || !route.resolved) return {};
8413
+ async function fetchPersonaPrompt(route2, role, opts) {
8414
+ const agentId = await resolveAgentId(route2, role, opts);
8415
+ if (!agentId || !route2.resolved) return {};
8416
8416
  const client2 = new TablationClient2({
8417
- baseUrl: `${route.baseUrl}/api`,
8418
- apiKey: resolveApiKey(route),
8417
+ baseUrl: `${route2.baseUrl}/api`,
8418
+ apiKey: resolveApiKey(route2),
8419
8419
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
8420
8420
  });
8421
8421
  let row;
8422
8422
  let agentsModelId;
8423
8423
  try {
8424
- const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
8424
+ const agentsModel = await client2.dataModels.get("agents", route2.resolved.workspaceId);
8425
8425
  agentsModelId = agentsModel.id;
8426
8426
  row = await client2.records.get(agentsModelId, agentId);
8427
8427
  } catch {
8428
8428
  return {};
8429
8429
  }
8430
8430
  const additions = typeof row.prompt_additions === "string" && row.prompt_additions.trim() !== "" ? row.prompt_additions : void 0;
8431
- const divergedPrompt = await divergedRowPrompt(route, role, row, agentId, agentsModelId, opts);
8431
+ const divergedPrompt = await divergedRowPrompt(route2, role, row, agentId, agentsModelId, opts);
8432
8432
  return { divergedPrompt, additions };
8433
8433
  }
8434
8434
  function personaPromptOptions(p) {
8435
8435
  return { divergedPrompt: p.divergedPrompt, personaAdditions: p.additions };
8436
8436
  }
8437
- async function divergedRowPrompt(route, role, row, agentId, agentsModelId, opts) {
8438
- if (!route.resolved) return void 0;
8439
- const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
8437
+ async function divergedRowPrompt(route2, role, row, agentId, agentsModelId, opts) {
8438
+ if (!route2.resolved) return void 0;
8439
+ const defaultPrompt = personaDefaultPrompt(route2.promptsDir, role);
8440
8440
  if (row.prompt === defaultPrompt) return void 0;
8441
- const cached = route.resolved.agentPersonas?.[role];
8441
+ const cached = route2.resolved.agentPersonas?.[role];
8442
8442
  if (cached?.agentId === agentId && cached.historyId) {
8443
- const lastSynced = await fetchLastSyncedPrompt(route, agentsModelId, agentId, cached.historyId, opts.userAgent);
8443
+ const lastSynced = await fetchLastSyncedPrompt(route2, agentsModelId, agentId, cached.historyId, opts.userAgent);
8444
8444
  if (lastSynced !== void 0 && lastSynced === row.prompt) return void 0;
8445
8445
  }
8446
8446
  return row.prompt;
8447
8447
  }
8448
- async function fetchLastSyncedPrompt(route, agentsModelId, recordId, historyId, userAgent) {
8448
+ async function fetchLastSyncedPrompt(route2, agentsModelId, recordId, historyId, userAgent) {
8449
8449
  try {
8450
- const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent);
8450
+ const entries = await fetchHistoryEntries(route2, agentsModelId, recordId, userAgent);
8451
8451
  return entries.find((e) => e.history_id === historyId)?.prompt;
8452
8452
  } catch {
8453
8453
  return void 0;
@@ -8457,16 +8457,16 @@ function choiceValue(v) {
8457
8457
  const raw = typeof v === "string" ? v : v && typeof v === "object" ? v.value ?? v.key : void 0;
8458
8458
  return typeof raw === "string" && raw.trim() ? raw.trim().toLowerCase() : void 0;
8459
8459
  }
8460
- async function fetchSeatPersona(route, role, opts) {
8461
- const agentId = await resolveAgentId(route, role, opts);
8462
- if (!agentId || !route.resolved) return void 0;
8460
+ async function fetchSeatPersona(route2, role, opts) {
8461
+ const agentId = await resolveAgentId(route2, role, opts);
8462
+ if (!agentId || !route2.resolved) return void 0;
8463
8463
  const client2 = new TablationClient2({
8464
- baseUrl: `${route.baseUrl}/api`,
8465
- apiKey: resolveApiKey(route),
8464
+ baseUrl: `${route2.baseUrl}/api`,
8465
+ apiKey: resolveApiKey(route2),
8466
8466
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
8467
8467
  });
8468
8468
  try {
8469
- const agentsModel = await client2.dataModels.get("agents", route.resolved.workspaceId);
8469
+ const agentsModel = await client2.dataModels.get("agents", route2.resolved.workspaceId);
8470
8470
  const row = await client2.records.get(agentsModel.id, agentId);
8471
8471
  const tier = choiceValue(row.tier);
8472
8472
  const vendor = choiceValue(row.vendor);
@@ -8480,24 +8480,24 @@ async function fetchSeatPersona(route, role, opts) {
8480
8480
  return void 0;
8481
8481
  }
8482
8482
  }
8483
- async function currentPersonaPrompt(route, role, opts) {
8484
- const { divergedPrompt, additions } = await fetchPersonaPrompt(route, role, opts);
8485
- return appendAdditions(divergedPrompt ?? personaDefaultPrompt(route.promptsDir, role), additions);
8483
+ async function currentPersonaPrompt(route2, role, opts) {
8484
+ const { divergedPrompt, additions } = await fetchPersonaPrompt(route2, role, opts);
8485
+ return appendAdditions(divergedPrompt ?? personaDefaultPrompt(route2.promptsDir, role), additions);
8486
8486
  }
8487
- async function fetchHistoryEntries(route, agentsModelId, recordId, userAgent, limit = 50) {
8488
- const url = `${route.baseUrl.replace(/\/+$/, "")}/api/data-models/${agentsModelId}/records/${recordId}/history?limit=${limit}`;
8487
+ async function fetchHistoryEntries(route2, agentsModelId, recordId, userAgent, limit = 50) {
8488
+ const url = `${route2.baseUrl.replace(/\/+$/, "")}/api/data-models/${agentsModelId}/records/${recordId}/history?limit=${limit}`;
8489
8489
  const res = await fetch(url, {
8490
8490
  headers: {
8491
- Authorization: `Bearer ${resolveApiKey(route)}`,
8491
+ Authorization: `Bearer ${resolveApiKey(route2)}`,
8492
8492
  "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
8493
8493
  }
8494
8494
  });
8495
8495
  if (!res.ok) throw new AgentsSyncError(`history lookup for ${recordId}: ${res.status} ${res.statusText}`);
8496
8496
  return await res.json();
8497
8497
  }
8498
- async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
8498
+ async function fetchLatestHistoryId(route2, agentsModelId, recordId, userAgent) {
8499
8499
  try {
8500
- const entries = await fetchHistoryEntries(route, agentsModelId, recordId, userAgent, 1);
8500
+ const entries = await fetchHistoryEntries(route2, agentsModelId, recordId, userAgent, 1);
8501
8501
  return entries[0]?.history_id;
8502
8502
  } catch {
8503
8503
  return void 0;
@@ -8506,11 +8506,11 @@ async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
8506
8506
  function isAdminRole(role) {
8507
8507
  return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
8508
8508
  }
8509
- async function fetchKeyRole(route, userAgent) {
8510
- if (!route.resolved) return void 0;
8509
+ async function fetchKeyRole(route2, userAgent) {
8510
+ if (!route2.resolved) return void 0;
8511
8511
  try {
8512
- const res = await fetch(`${route.baseUrl.replace(/\/+$/, "")}/api/auth/me?workspaceId=${route.resolved.workspaceId}`, {
8513
- headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
8512
+ const res = await fetch(`${route2.baseUrl.replace(/\/+$/, "")}/api/auth/me?workspaceId=${route2.resolved.workspaceId}`, {
8513
+ headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
8514
8514
  });
8515
8515
  if (!res.ok) return void 0;
8516
8516
  const me = await res.json();
@@ -8536,14 +8536,14 @@ function isPromptGateRejection(e) {
8536
8536
  }
8537
8537
  var RELEASE_AGENT_NAME = "Release agent";
8538
8538
  var RELEASE_AGENT_PROMPT = "Principal for the crew release phase (merge, deploy, release comments). It runs no model; this row only gives those tracker writes an agent identity.";
8539
- async function syncPersonas(route, opts) {
8540
- if (!route.resolved) {
8541
- throw new AgentsSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
8539
+ async function syncPersonas(route2, opts) {
8540
+ if (!route2.resolved) {
8541
+ throw new AgentsSyncError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
8542
8542
  }
8543
- const workspaceId = route.resolved.workspaceId;
8543
+ const workspaceId = route2.resolved.workspaceId;
8544
8544
  const client2 = new TablationClient2({
8545
- baseUrl: `${route.baseUrl}/api`,
8546
- apiKey: resolveApiKey(route),
8545
+ baseUrl: `${route2.baseUrl}/api`,
8546
+ apiKey: resolveApiKey(route2),
8547
8547
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
8548
8548
  });
8549
8549
  let agentsModel;
@@ -8558,12 +8558,12 @@ async function syncPersonas(route, opts) {
8558
8558
  throw e;
8559
8559
  }
8560
8560
  const rows = await client2.records.list(agentsModel.id, { limit: 200 });
8561
- const cache = route.resolved.agentPersonas ?? {};
8561
+ const cache = route2.resolved.agentPersonas ?? {};
8562
8562
  const outcomes = [];
8563
8563
  const agentPersonas = { ...cache };
8564
8564
  for (const role of ROLE_NAMES) {
8565
8565
  const name = PERSONA_NAME[role];
8566
- const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
8566
+ const defaultPrompt = personaDefaultPrompt(route2.promptsDir, role);
8567
8567
  const row = rows.find((r) => r.name === name);
8568
8568
  if (opts.readOnly) {
8569
8569
  if (!row) {
@@ -8581,7 +8581,7 @@ async function syncPersonas(route, opts) {
8581
8581
  continue;
8582
8582
  }
8583
8583
  const created = await client2.records.create(agentsModel.id, { name, prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) });
8584
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, created.id, opts.userAgent);
8584
+ const historyId = await fetchLatestHistoryId(route2, agentsModel.id, created.id, opts.userAgent);
8585
8585
  agentPersonas[role] = { agentId: created.id, lastSyncedUpdatedAt: created.updated_at, ...historyId ? { historyId } : {} };
8586
8586
  outcomes.push({ role, action: "created", agentId: created.id });
8587
8587
  continue;
@@ -8595,7 +8595,7 @@ async function syncPersonas(route, opts) {
8595
8595
  if (!(e instanceof StaleWriteError2) && !isPromptGateRejection(e)) throw e;
8596
8596
  }
8597
8597
  }
8598
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
8598
+ const historyId = await fetchLatestHistoryId(route2, agentsModel.id, row.id, opts.userAgent);
8599
8599
  agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: anchoredAt, ...historyId ? { historyId } : {} };
8600
8600
  outcomes.push({ role, action: "unchanged", agentId: row.id });
8601
8601
  continue;
@@ -8620,7 +8620,7 @@ async function syncPersonas(route, opts) {
8620
8620
  agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: row.updated_at, ...cache[role]?.agentId === row.id && cache[role]?.historyId ? { historyId: cache[role].historyId } : {} };
8621
8621
  continue;
8622
8622
  }
8623
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
8623
+ const historyId = await fetchLatestHistoryId(route2, agentsModel.id, row.id, opts.userAgent);
8624
8624
  agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
8625
8625
  outcomes.push({ role, action: "forced", agentId: row.id, previousPrompt: row.prompt });
8626
8626
  continue;
@@ -8639,7 +8639,7 @@ async function syncPersonas(route, opts) {
8639
8639
  }
8640
8640
  let lastSyncedPrompt;
8641
8641
  try {
8642
- const entries = await fetchHistoryEntries(route, agentsModel.id, row.id, opts.userAgent);
8642
+ const entries = await fetchHistoryEntries(route2, agentsModel.id, row.id, opts.userAgent);
8643
8643
  lastSyncedPrompt = entries.find((e) => e.history_id === cached.historyId)?.prompt;
8644
8644
  } catch {
8645
8645
  lastSyncedPrompt = void 0;
@@ -8660,7 +8660,7 @@ async function syncPersonas(route, opts) {
8660
8660
  { prompt: defaultPrompt, ...promptSourceFields(agentsModel, defaultPrompt) },
8661
8661
  row.updated_at
8662
8662
  );
8663
- const historyId = await fetchLatestHistoryId(route, agentsModel.id, row.id, opts.userAgent);
8663
+ const historyId = await fetchLatestHistoryId(route2, agentsModel.id, row.id, opts.userAgent);
8664
8664
  agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: updated.updated_at, ...historyId ? { historyId } : {} };
8665
8665
  outcomes.push({ role, action: "updated", agentId: row.id });
8666
8666
  } catch (e) {
@@ -8676,8 +8676,8 @@ async function syncPersonas(route, opts) {
8676
8676
  throw e;
8677
8677
  }
8678
8678
  }
8679
- const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent, opts.readOnly === true);
8680
- const release = opts.dryRun ? { agentId: route.resolved.releaseAgentId } : await linkReleaseSeat(route, client2, agentsModel, opts.userAgent, opts.readOnly === true);
8679
+ const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route2, client2, agentsModel.id, agentPersonas, opts.userAgent, opts.readOnly === true);
8680
+ const release = opts.dryRun ? { agentId: route2.resolved.releaseAgentId } : await linkReleaseSeat(route2, client2, agentsModel, opts.userAgent, opts.readOnly === true);
8681
8681
  return {
8682
8682
  outcomes,
8683
8683
  agentPersonas,
@@ -8687,8 +8687,8 @@ async function syncPersonas(route, opts) {
8687
8687
  ...opts.readOnly ? { readOnly: true } : {}
8688
8688
  };
8689
8689
  }
8690
- async function linkReleaseSeat(route, client2, agentsModel, userAgent, readOnly) {
8691
- const seatId = route.resolved.releaseSeat;
8690
+ async function linkReleaseSeat(route2, client2, agentsModel, userAgent, readOnly) {
8691
+ const seatId = route2.resolved.releaseSeat;
8692
8692
  if (!seatId) return {};
8693
8693
  const rows = await client2.records.list(agentsModel.id, { limit: 200 });
8694
8694
  let agentId = rows.find((r) => r.name === RELEASE_AGENT_NAME)?.id;
@@ -8705,11 +8705,11 @@ async function linkReleaseSeat(route, client2, agentsModel, userAgent, readOnly)
8705
8705
  throw e;
8706
8706
  }
8707
8707
  }
8708
- const crewModelId = route.resolved.models.crew;
8708
+ const crewModelId = route2.resolved.models.crew;
8709
8709
  const crewModel = await client2.dataModels.get(crewModelId);
8710
8710
  let columnName;
8711
8711
  try {
8712
- columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModel.id, userAgent);
8712
+ columnName = await ensureCrewAgentField(route2, client2, crewModel, agentsModel.id, userAgent);
8713
8713
  } catch (e) {
8714
8714
  if (e instanceof CrewAgentFieldMissing) return { note: `Release: ${CREW_FIELD_MISSING_NOTE}` };
8715
8715
  throw e;
@@ -8722,18 +8722,18 @@ var NO_WRITE_NOTE = "this key has no write access to the Agents table";
8722
8722
  var CREW_FIELD_MISSING_NOTE = "Crew table has no Agent field yet \u2014 a workspace admin's `crew agents sync` adds it";
8723
8723
  var CrewAgentFieldMissing = class extends Error {
8724
8724
  };
8725
- async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent, readOnly) {
8726
- const seats2 = route.resolved.seats;
8725
+ async function linkCrewSeats(route2, client2, agentsModelId, agentPersonas, userAgent, readOnly) {
8726
+ const seats2 = route2.resolved.seats;
8727
8727
  const rolesWithSeats = ROLE_NAMES.filter((role) => seats2[role] && agentPersonas[role]);
8728
8728
  if (rolesWithSeats.length === 0) {
8729
8729
  if (readOnly) return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agents_row" : "no_seat" }));
8730
8730
  return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "persona_diverged" : "no_seat" }));
8731
8731
  }
8732
- const crewModelId = route.resolved.models.crew;
8732
+ const crewModelId = route2.resolved.models.crew;
8733
8733
  const crewModel = await client2.dataModels.get(crewModelId);
8734
8734
  let columnName;
8735
8735
  try {
8736
- columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent);
8736
+ columnName = await ensureCrewAgentField(route2, client2, crewModel, agentsModelId, userAgent);
8737
8737
  } catch (e) {
8738
8738
  if (e instanceof CrewAgentFieldMissing) {
8739
8739
  return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agent_field" : "no_seat" }));
@@ -8762,14 +8762,14 @@ async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userA
8762
8762
  }
8763
8763
  return outcomes;
8764
8764
  }
8765
- async function ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent) {
8765
+ async function ensureCrewAgentField(route2, client2, crewModel, agentsModelId, userAgent) {
8766
8766
  const existing = crewModel.fields.find(
8767
8767
  (f) => f.fieldType?.kind === "REFERENCE" && f.fieldType.targetModelId === agentsModelId && f.fieldType.isMultiple === false
8768
8768
  );
8769
8769
  if (existing) return existing.columnName;
8770
- const workspaceId = route.resolved.workspaceId;
8771
- const baseUrl = route.baseUrl.replace(/\/+$/, "");
8772
- const apiKey = resolveApiKey(route);
8770
+ const workspaceId = route2.resolved.workspaceId;
8771
+ const baseUrl = route2.baseUrl.replace(/\/+$/, "");
8772
+ const apiKey = resolveApiKey(route2);
8773
8773
  const headers = { Authorization: `Bearer ${apiKey}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" };
8774
8774
  const typesRes = await fetch(`${baseUrl}/api/workspaces/${workspaceId}/field-types`, { headers });
8775
8775
  if (!typesRes.ok) throw new AgentsSyncError(`field-types lookup failed: ${typesRes.status} ${typesRes.statusText}`);
@@ -8855,14 +8855,14 @@ function listLocalSkills(promptsDir) {
8855
8855
  if (!existsSync19(dir)) return [];
8856
8856
  return readdirSync8(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join20(dir, f)));
8857
8857
  }
8858
- async function syncSkills(route, opts) {
8859
- if (!route.resolved) {
8860
- throw new SkillSyncError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
8858
+ async function syncSkills(route2, opts) {
8859
+ if (!route2.resolved) {
8860
+ throw new SkillSyncError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
8861
8861
  }
8862
- const workspaceId = route.resolved.workspaceId;
8862
+ const workspaceId = route2.resolved.workspaceId;
8863
8863
  const client2 = new TablationClient3({
8864
- baseUrl: `${route.baseUrl}/api`,
8865
- apiKey: resolveApiKey(route),
8864
+ baseUrl: `${route2.baseUrl}/api`,
8865
+ apiKey: resolveApiKey(route2),
8866
8866
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
8867
8867
  });
8868
8868
  let skillsModel;
@@ -8876,9 +8876,9 @@ async function syncSkills(route, opts) {
8876
8876
  }
8877
8877
  throw e;
8878
8878
  }
8879
- const local = listLocalSkills(route.promptsDir);
8879
+ const local = listLocalSkills(route2.promptsDir);
8880
8880
  const rows = await client2.records.list(skillsModel.id, { limit: 200 });
8881
- const cache = route.resolved.agentSkills ?? {};
8881
+ const cache = route2.resolved.agentSkills ?? {};
8882
8882
  const outcomes = [];
8883
8883
  const agentSkills = { ...cache };
8884
8884
  for (const skill of local) {
@@ -8994,22 +8994,22 @@ function describeSkillSyncOutcome(o) {
8994
8994
  return `${o.name}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.skillId} in the app before syncing again`;
8995
8995
  }
8996
8996
  }
8997
- async function fetchSkillOverrides(route, opts) {
8998
- if (!route.resolved) return {};
8997
+ async function fetchSkillOverrides(route2, opts) {
8998
+ if (!route2.resolved) return {};
8999
8999
  const client2 = new TablationClient3({
9000
- baseUrl: `${route.baseUrl}/api`,
9001
- apiKey: resolveApiKey(route),
9000
+ baseUrl: `${route2.baseUrl}/api`,
9001
+ apiKey: resolveApiKey(route2),
9002
9002
  headers: { "User-Agent": opts.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9003
9003
  });
9004
9004
  let rows;
9005
9005
  try {
9006
- const model = await client2.dataModels.get("agent_skills", route.resolved.workspaceId);
9006
+ const model = await client2.dataModels.get("agent_skills", route2.resolved.workspaceId);
9007
9007
  rows = await client2.records.list(model.id, { limit: 200 });
9008
9008
  } catch {
9009
9009
  return {};
9010
9010
  }
9011
- const local = new Map(listLocalSkills(route.promptsDir).map((s) => [s.name, s]));
9012
- const cache = route.resolved.agentSkills ?? {};
9011
+ const local = new Map(listLocalSkills(route2.promptsDir).map((s) => [s.name, s]));
9012
+ const cache = route2.resolved.agentSkills ?? {};
9013
9013
  const out = {};
9014
9014
  for (const row of rows) {
9015
9015
  const name = row.name;
@@ -9625,10 +9625,10 @@ function spawnAgent(plan, emit, hintMoved) {
9625
9625
  const tail3 = new FileTail(plan.streamPath);
9626
9626
  const timer = setInterval(() => obs.feed(tail3.poll()), TAIL_POLL_MS);
9627
9627
  let spawnFailed = false;
9628
- child.on("error", (err) => {
9628
+ child.on("error", (err2) => {
9629
9629
  spawnFailed = true;
9630
9630
  clearInterval(timer);
9631
- reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err.message}`));
9631
+ reject(new AgentSpawnError(`cannot run ${plan.bin}: ${err2.message}`));
9632
9632
  });
9633
9633
  child.on("close", async (code) => {
9634
9634
  clearInterval(timer);
@@ -9639,8 +9639,8 @@ function spawnAgent(plan, emit, hintMoved) {
9639
9639
  const { ms: ms3, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
9640
9640
  resolve6({ code: code ?? 1, ms: ms3, ...handoffPending ? { handoffPending } : {} });
9641
9641
  });
9642
- child.stdin?.on("error", (err) => {
9643
- emit.warn(`agent closed stdin before the prompt was delivered: ${err.message}`, { step: "agent" });
9642
+ child.stdin?.on("error", (err2) => {
9643
+ emit.warn(`agent closed stdin before the prompt was delivered: ${err2.message}`, { step: "agent" });
9644
9644
  });
9645
9645
  child.stdin?.end(plan.prompt);
9646
9646
  });
@@ -9682,8 +9682,8 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
9682
9682
  // src/seat-keys.ts
9683
9683
  import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
9684
9684
  var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
9685
- function keyIsAdmin(route) {
9686
- const role = route.resolved?.role;
9685
+ function keyIsAdmin(route2) {
9686
+ const role = route2.resolved?.role;
9687
9687
  return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
9688
9688
  }
9689
9689
  var MEMBER_NO_ROW = "has no linked Agents row \u2014 a workspace admin's `crew agents sync` creates the persona rows; run it here afterwards to link this ship's seats";
@@ -9692,41 +9692,41 @@ var FORBIDDEN_LINE = 'seat keys: not minted \u2014 your role does not grant "Min
9692
9692
  function seatKeySlug(workspaceSlug, agentId) {
9693
9693
  return `${workspaceSlug}#agent:${agentId}`;
9694
9694
  }
9695
- function slugFor(route, agentId) {
9696
- const [workspaceSlug] = splitRoute(route.route);
9695
+ function slugFor(route2, agentId) {
9696
+ const [workspaceSlug] = splitRoute(route2.route);
9697
9697
  return seatKeySlug(workspaceSlug, agentId);
9698
9698
  }
9699
- function seatAgentId(route, role) {
9700
- return route.resolved?.agentPersonas?.[role]?.agentId;
9699
+ function seatAgentId(route2, role) {
9700
+ return route2.resolved?.agentPersonas?.[role]?.agentId;
9701
9701
  }
9702
- async function resolveReleaseKey(route, getStore = getSessionStore2) {
9703
- const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
9704
- const agentId = route.resolved?.releaseAgentId;
9705
- if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
9702
+ async function resolveReleaseKey(route2, getStore = getSessionStore2) {
9703
+ const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
9704
+ const agentId = route2.resolved?.releaseAgentId;
9705
+ if (!agentId) return ship(keyIsAdmin(route2) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
9706
9706
  try {
9707
- const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
9707
+ const session = await getStore().get(hostFromUrl2(route2.baseUrl), slugFor(route2, agentId));
9708
9708
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
9709
- return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
9709
+ return ship(keyIsAdmin(route2) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
9710
9710
  } catch (e) {
9711
9711
  return ship(describeReadError(e));
9712
9712
  }
9713
9713
  }
9714
- async function resolveSeatKey(route, role, getStore = getSessionStore2) {
9715
- const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
9714
+ async function resolveSeatKey(route2, role, getStore = getSessionStore2) {
9715
+ const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route2), reason });
9716
9716
  if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
9717
- const agentId = seatAgentId(route, role);
9718
- if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
9717
+ const agentId = seatAgentId(route2, role);
9718
+ if (!agentId) return ship(keyIsAdmin(route2) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
9719
9719
  try {
9720
- const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
9720
+ const session = await getStore().get(hostFromUrl2(route2.baseUrl), slugFor(route2, agentId));
9721
9721
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
9722
- return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
9722
+ return ship(keyIsAdmin(route2) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
9723
9723
  } catch (e) {
9724
9724
  return ship(describeReadError(e));
9725
9725
  }
9726
9726
  }
9727
9727
  function safeStderrLine(e) {
9728
- const err = e;
9729
- const raw = typeof err?.stderr === "string" && err.stderr.trim() ? err.stderr : typeof err?.message === "string" ? err.message : "";
9728
+ const err2 = e;
9729
+ const raw = typeof err2?.stderr === "string" && err2.stderr.trim() ? err2.stderr : typeof err2?.message === "string" ? err2.message : "";
9730
9730
  for (const line of raw.split("\n")) {
9731
9731
  const t = line.trim();
9732
9732
  if (!t || /^Command failed:/i.test(t) || /(^|\s)-w(\s|$)/.test(t)) continue;
@@ -9752,13 +9752,13 @@ function describeStoreError(e) {
9752
9752
  }
9753
9753
  return "keychain unavailable (could not write to the OS keychain)";
9754
9754
  }
9755
- async function mintMissingSeatKeys(route, o) {
9755
+ async function mintMissingSeatKeys(route2, o) {
9756
9756
  const out = [];
9757
- if (!route.resolved) return out;
9758
- const resolved = route.resolved;
9757
+ if (!route2.resolved) return out;
9758
+ const resolved = route2.resolved;
9759
9759
  const doFetch = o.fetchImpl ?? fetch;
9760
9760
  const targets = [
9761
- ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
9761
+ ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
9762
9762
  ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
9763
9763
  ];
9764
9764
  let store;
@@ -9771,10 +9771,10 @@ async function mintMissingSeatKeys(route, o) {
9771
9771
  detail: describeStoreError(e)
9772
9772
  }));
9773
9773
  }
9774
- const host = hostFromUrl2(route.baseUrl);
9775
- const base = route.baseUrl.replace(/\/+$/, "");
9774
+ const host = hostFromUrl2(route2.baseUrl);
9775
+ const base = route2.baseUrl.replace(/\/+$/, "");
9776
9776
  if (!o.dryRun) {
9777
- const probeSlug = seatKeySlug(splitRoute(route.route)[0], "_probe");
9777
+ const probeSlug = seatKeySlug(splitRoute(route2.route)[0], "_probe");
9778
9778
  try {
9779
9779
  await store.set(host, probeSlug, {
9780
9780
  apiKey: "probe",
@@ -9793,7 +9793,7 @@ async function mintMissingSeatKeys(route, o) {
9793
9793
  out.push({ role, action: "no_agent" });
9794
9794
  continue;
9795
9795
  }
9796
- const slug = slugFor(route, agentId);
9796
+ const slug = slugFor(route2, agentId);
9797
9797
  try {
9798
9798
  if ((await store.get(host, slug))?.apiKey) {
9799
9799
  out.push({ role, action: "already_stored" });
@@ -9806,7 +9806,7 @@ async function mintMissingSeatKeys(route, o) {
9806
9806
  const res = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
9807
9807
  method: "POST",
9808
9808
  headers: {
9809
- Authorization: `Bearer ${resolveApiKey(route)}`,
9809
+ Authorization: `Bearer ${resolveApiKey(route2)}`,
9810
9810
  "Content-Type": "application/json",
9811
9811
  "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0"
9812
9812
  },
@@ -9840,7 +9840,7 @@ async function mintMissingSeatKeys(route, o) {
9840
9840
  try {
9841
9841
  const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${body.id}`, {
9842
9842
  method: "DELETE",
9843
- headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9843
+ headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9844
9844
  });
9845
9845
  revoked = del2.ok;
9846
9846
  } catch {
@@ -9875,13 +9875,13 @@ function describeMintOutcome(o) {
9875
9875
  return `${o.role}: could not mint an agent key (${o.detail ?? "unknown"}) \u2014 writes as the ship`;
9876
9876
  }
9877
9877
  }
9878
- async function revokeSeatKeys(route, o) {
9878
+ async function revokeSeatKeys(route2, o) {
9879
9879
  const out = [];
9880
- if (!route.resolved) return out;
9881
- const resolved = route.resolved;
9880
+ if (!route2.resolved) return out;
9881
+ const resolved = route2.resolved;
9882
9882
  const doFetch = o.fetchImpl ?? fetch;
9883
9883
  const targets = [
9884
- ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route, role) })),
9884
+ ...SEAT_KEY_ROLES.filter((r) => resolved.seats[r]).map((role) => ({ role, agentId: seatAgentId(route2, role) })),
9885
9885
  ...resolved.releaseSeat ? [{ role: "release", agentId: resolved.releaseAgentId }] : []
9886
9886
  ];
9887
9887
  let store;
@@ -9890,14 +9890,14 @@ async function revokeSeatKeys(route, o) {
9890
9890
  } catch (e) {
9891
9891
  return targets.map(({ role }) => ({ role, action: "failed", detail: describeStoreError(e) }));
9892
9892
  }
9893
- const host = hostFromUrl2(route.baseUrl);
9894
- const base = route.baseUrl.replace(/\/+$/, "");
9893
+ const host = hostFromUrl2(route2.baseUrl);
9894
+ const base = route2.baseUrl.replace(/\/+$/, "");
9895
9895
  for (const { role, agentId } of targets) {
9896
9896
  if (!agentId) {
9897
9897
  out.push({ role, action: "no_agent" });
9898
9898
  continue;
9899
9899
  }
9900
- const slug = slugFor(route, agentId);
9900
+ const slug = slugFor(route2, agentId);
9901
9901
  try {
9902
9902
  const session = await store.get(host, slug);
9903
9903
  if (!session?.apiKey) {
@@ -9914,7 +9914,7 @@ async function revokeSeatKeys(route, o) {
9914
9914
  }
9915
9915
  const del2 = await doFetch(`${base}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys/${session.keyId}`, {
9916
9916
  method: "DELETE",
9917
- headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9917
+ headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9918
9918
  });
9919
9919
  if (!del2.ok && del2.status !== 404) {
9920
9920
  out.push({ role, action: "failed", detail: `${del2.status} ${del2.statusText}` });
@@ -9940,14 +9940,14 @@ function describeRevokeOutcome(o) {
9940
9940
  return `${o.role}: could not revoke the agent key (${o.detail ?? "unknown"}) \u2014 left in the keychain`;
9941
9941
  }
9942
9942
  }
9943
- async function canMintAgentKeys(route, o = {}) {
9944
- const resolved = route.resolved;
9943
+ async function canMintAgentKeys(route2, o = {}) {
9944
+ const resolved = route2.resolved;
9945
9945
  if (!resolved) return void 0;
9946
- const agentId = seatAgentId(route, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route, r)).find(Boolean);
9946
+ const agentId = seatAgentId(route2, "dev") ?? SEAT_KEY_ROLES.map((r) => seatAgentId(route2, r)).find(Boolean);
9947
9947
  if (!agentId) return void 0;
9948
9948
  try {
9949
- const res = await (o.fetchImpl ?? fetch)(`${route.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
9950
- headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9949
+ const res = await (o.fetchImpl ?? fetch)(`${route2.baseUrl.replace(/\/+$/, "")}/api/workspaces/${resolved.workspaceId}/agents/${agentId}/api-keys`, {
9950
+ headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
9951
9951
  });
9952
9952
  if (res.ok) return true;
9953
9953
  return res.status === 403 ? false : void 0;
@@ -10175,7 +10175,7 @@ function doctorSelftestLines(stored, toolchains) {
10175
10175
  }
10176
10176
 
10177
10177
  // src/cli.ts
10178
- import { resolve as resolve5, dirname as dirname21, join as join42, basename as basename12 } from "path";
10178
+ import { resolve as resolve5, dirname as dirname22, join as join43, basename as basename13 } from "path";
10179
10179
  import { fileURLToPath } from "url";
10180
10180
  import { hostname as hostname2, homedir as homedir10 } from "os";
10181
10181
  import { spawnSync as spawnSync7 } from "child_process";
@@ -10246,12 +10246,12 @@ function splitWorktreeArgs(positional2, routeNames) {
10246
10246
  if (a !== void 0 && isTicket(a) && !routeNames.includes(a)) return { ticket: a };
10247
10247
  return { route: a, ticket: void 0 };
10248
10248
  }
10249
- function pickWorktreeRoute(ticketNumber, candidates) {
10250
- for (const c of candidates) {
10251
- if (c.hit && (candidates.length === 1 || c.servedRepoIds.includes(c.hit.repoId ?? ""))) return { route: c.route };
10249
+ function pickWorktreeRoute(ticketNumber, candidates2) {
10250
+ for (const c of candidates2) {
10251
+ if (c.hit && (candidates2.length === 1 || c.servedRepoIds.includes(c.hit.repoId ?? ""))) return { route: c.route };
10252
10252
  }
10253
- const looked = candidates.map((c) => c.route).join(", ");
10254
- const found = candidates.filter((c) => c.hit).map((c) => `${c.route} (repo ${c.hit?.repoId ?? "none"})`);
10253
+ const looked = candidates2.map((c) => c.route).join(", ");
10254
+ const found = candidates2.filter((c) => c.hit).map((c) => `${c.route} (repo ${c.hit?.repoId ?? "none"})`);
10255
10255
  return {
10256
10256
  error: `no open ticket ISSUE-${ticketNumber} served by this ship \u2014 looked on ${looked}` + (found.length ? `; found it only on ${found.join(", ")}, which has no checkout for its repo` : "")
10257
10257
  };
@@ -10502,22 +10502,22 @@ async function runReset(o, fx) {
10502
10502
  for (const url of urls) {
10503
10503
  for (const line of fx.removePair(url, o.dryRun)) steps.push({ step: "pair", target: line, action: mark(o.dryRun) });
10504
10504
  }
10505
- for (const route of o.routes) {
10506
- for (const r of await fx.revokeSeatKeys(route, o.dryRun)) {
10507
- steps.push({ step: "seat-keys", target: `${route.route}: ${r.line}`, action: r.ok ? mark(o.dryRun) : "failed" });
10505
+ for (const route2 of o.routes) {
10506
+ for (const r of await fx.revokeSeatKeys(route2, o.dryRun)) {
10507
+ steps.push({ step: "seat-keys", target: `${route2.route}: ${r.line}`, action: r.ok ? mark(o.dryRun) : "failed" });
10508
10508
  }
10509
10509
  }
10510
- for (const route of [...o.routes, ...o.droppedRoutes ?? []]) {
10510
+ for (const route2 of [...o.routes, ...o.droppedRoutes ?? []]) {
10511
10511
  try {
10512
- const cred = await fx.credential(route);
10512
+ const cred = await fx.credential(route2);
10513
10513
  if (!cred.stored) {
10514
- steps.push({ step: "credential", target: route.route, action: "absent", detail: "no workspace credential in the keychain" });
10514
+ steps.push({ step: "credential", target: route2.route, action: "absent", detail: "no workspace credential in the keychain" });
10515
10515
  continue;
10516
10516
  }
10517
10517
  if (!o.dryRun) await cred.clear();
10518
- steps.push({ step: "credential", target: route.route, action: mark(o.dryRun), detail: "workspace credential in the keychain" });
10518
+ steps.push({ step: "credential", target: route2.route, action: mark(o.dryRun), detail: "workspace credential in the keychain" });
10519
10519
  } catch (e) {
10520
- steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
10520
+ steps.push({ step: "credential", target: route2.route, action: "failed", detail: e.message });
10521
10521
  }
10522
10522
  }
10523
10523
  const covered = new Set([...o.routes, ...o.droppedRoutes ?? []].map((r) => `${hostFromUrl3(r.baseUrl)}/${splitRoute(r.route)[0]}`));
@@ -10618,12 +10618,12 @@ function outdatedItem(have, floor, nowIso = (/* @__PURE__ */ new Date()).toISOSt
10618
10618
  function sentVersion(ship) {
10619
10619
  return /CrewAgent\/(\d+\.\d+\.\d+\S*)/.exec(ship.userAgent)?.[1] ?? null;
10620
10620
  }
10621
- async function readCrewFloor(route, ship) {
10622
- const r = route.resolved;
10621
+ async function readCrewFloor(route2, ship) {
10622
+ const r = route2.resolved;
10623
10623
  if (!r?.projectId) return void 0;
10624
10624
  try {
10625
- const res = await fetch(`${route.baseUrl}/api/projects/${encodeURIComponent(r.projectId)}?workspaceId=${r.workspaceId}`, {
10626
- headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": ship.userAgent }
10625
+ const res = await fetch(`${route2.baseUrl}/api/projects/${encodeURIComponent(r.projectId)}?workspaceId=${r.workspaceId}`, {
10626
+ headers: { Authorization: `Bearer ${resolveApiKey(route2)}`, "User-Agent": ship.userAgent }
10627
10627
  });
10628
10628
  if (!res.ok) return void 0;
10629
10629
  const body = await res.json();
@@ -10648,12 +10648,12 @@ async function checkCrewFloor(o, deps = {}) {
10648
10648
  function openOutdated(state) {
10649
10649
  return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
10650
10650
  }
10651
- function isUpgradeRequired(err) {
10652
- return err instanceof TablationApiError3 && err.status === 426;
10651
+ function isUpgradeRequired(err2) {
10652
+ return err2 instanceof TablationApiError3 && err2.status === 426;
10653
10653
  }
10654
- async function raiseFromUpgradeRequired(o, err) {
10654
+ async function raiseFromUpgradeRequired(o, err2) {
10655
10655
  const have = sentVersion(o.ship) ?? "unknown";
10656
- const floor = /(\d+\.\d+\.\d+)/.exec(err.message.replace(have, ""))?.[1] ?? "a newer version";
10656
+ const floor = /(\d+\.\d+\.\d+)/.exec(err2.message.replace(have, ""))?.[1] ?? "a newer version";
10657
10657
  await raiseShipAttention(o, outdatedItem(have, floor));
10658
10658
  }
10659
10659
 
@@ -10963,10 +10963,10 @@ function rankedCandidates(role, i) {
10963
10963
  }
10964
10964
  function roleTopRank(role, i) {
10965
10965
  if (role !== "qa" && role !== "triage" && !i.seats[role]) return NOTHING_ACTIONABLE;
10966
- const candidates = roleCandidates(role, i);
10967
- if (candidates.length === 0) return NOTHING_ACTIONABLE;
10966
+ const candidates2 = roleCandidates(role, i);
10967
+ if (candidates2.length === 0) return NOTHING_ACTIONABLE;
10968
10968
  const c = i.contract ?? DEFAULT_CONTRACT;
10969
- return Math.min(...candidates.map((t) => rankScalar(t, c)));
10969
+ return Math.min(...candidates2.map((t) => rankScalar(t, c)));
10970
10970
  }
10971
10971
  function actionableSummary(i, pending) {
10972
10972
  const c = i.contract ?? DEFAULT_CONTRACT;
@@ -11461,15 +11461,15 @@ async function runStalledSweep(o, stalled) {
11461
11461
 
11462
11462
  // src/poll.ts
11463
11463
  async function decideCycle(o) {
11464
- const { route, ship, state, emit } = o;
11465
- const tracker = new Tracker(route, ship);
11464
+ const { route: route2, ship, state, emit } = o;
11465
+ const tracker = new Tracker(route2, ship);
11466
11466
  tracker.onReadFailure = (m) => emit.warn(m);
11467
11467
  emit.enter("poll");
11468
11468
  const optional = async (what, read, fallback) => {
11469
11469
  try {
11470
11470
  return await read;
11471
- } catch (err) {
11472
- emit.warn(`reading ${what} failed: ${err instanceof Error ? err.message : String(err)}`);
11471
+ } catch (err2) {
11472
+ emit.warn(`reading ${what} failed: ${err2 instanceof Error ? err2.message : String(err2)}`);
11473
11473
  return fallback;
11474
11474
  }
11475
11475
  };
@@ -11493,13 +11493,13 @@ async function decideCycle(o) {
11493
11493
  const epicStatusById = new Map(epicRows.map((e) => [e.id, e[epicStatusColumn] ?? null]));
11494
11494
  let epicSync = [];
11495
11495
  try {
11496
- const candidates = trackedEpics(epicRows, tracker.contract);
11497
- const outstanding = await tracker.epicTickets(candidates.map((e) => e.id), { closed: false });
11496
+ const candidates2 = trackedEpics(epicRows, tracker.contract);
11497
+ const outstanding = await tracker.epicTickets(candidates2.map((e) => e.id), { closed: false });
11498
11498
  const closed = await tracker.epicTickets(
11499
- epicsNeedingClosedCheck(candidates, outstanding, tracker.contract),
11499
+ epicsNeedingClosedCheck(candidates2, outstanding, tracker.contract),
11500
11500
  { closed: true }
11501
11501
  );
11502
- epicSync = planEpicSync(candidates, outstanding, closed, tracker.contract);
11502
+ epicSync = planEpicSync(candidates2, outstanding, closed, tracker.contract);
11503
11503
  for (const s of epicSync) epicStatusById.set(s.epic.id, s.to);
11504
11504
  } catch (e) {
11505
11505
  emit.warn(`could not take stock of epics this cycle: ${e.message}`);
@@ -11515,7 +11515,7 @@ async function decideCycle(o) {
11515
11515
  const info = blockerInfoMap(tickets, extra);
11516
11516
  const blocked = computeBlockedIds(tickets, info);
11517
11517
  emit.enter("sweep");
11518
- const roster = buildRoster(configuredMembers(route), crewRows);
11518
+ const roster = buildRoster(configuredMembers(route2), crewRows);
11519
11519
  const holds2 = new Set(holdIds(roster));
11520
11520
  const workModes = { modes: projectModes, agentAssignees: agentAssigneeIds(crewRows, (id) => holds2.has(id)) };
11521
11521
  const diag = sweepDiagnostics(tickets, info);
@@ -11558,7 +11558,7 @@ async function decideCycle(o) {
11558
11558
  const unroutable = unroutableTickets.map((t) => t.id);
11559
11559
  const { added, gained } = unroutableChanges(
11560
11560
  unroutableTickets,
11561
- state.unroutable(route.route).previous(),
11561
+ state.unroutable(route2.route).previous(),
11562
11562
  tickets,
11563
11563
  tracker.contract.columns.externalId
11564
11564
  );
@@ -11580,14 +11580,14 @@ async function decideCycle(o) {
11580
11580
  const reasons = attentionReasons(t, tracker.contract);
11581
11581
  if (reasons.length) attentionCurrent[t.id] = reasons;
11582
11582
  }
11583
- const attentionPrevious = state.attention(route.route).previous();
11583
+ const attentionPrevious = state.attention(route2.route).previous();
11584
11584
  const transitions2 = attentionTransitions(attentionPrevious, attentionCurrent);
11585
11585
  let mintFailed = false;
11586
11586
  for (const tr of transitions2) {
11587
11587
  const ticket = tickets.find((t) => t.id === tr.ticketId);
11588
11588
  if (!ticket) continue;
11589
11589
  const url = await tracker.recordLinkUrl(ticket.id);
11590
- if (!url && route.resolved?.recordLinkViewId) mintFailed = true;
11590
+ if (!url && route2.resolved?.recordLinkViewId) mintFailed = true;
11591
11591
  emit.emit(
11592
11592
  `needs a person: ${tr.reasons.join(", ")} \u2014 ${ticket.title}`,
11593
11593
  {
@@ -11606,15 +11606,15 @@ async function decideCycle(o) {
11606
11606
  }
11607
11607
  emit.enter("select");
11608
11608
  const watermark = state.watermark();
11609
- for (const repo of recheckBaseStops(state, route)) {
11609
+ for (const repo of recheckBaseStops(state, route2)) {
11610
11610
  emit.emit(`${repo}: primary checkout is level again \u2014 building lanes resume for that repo`, { step: "select" });
11611
11611
  }
11612
- for (const what of recheckEnvironmentStops(state, route, ship)) {
11612
+ for (const what of recheckEnvironmentStops(state, route2, ship)) {
11613
11613
  emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
11614
11614
  }
11615
- await checkRepoDisks({ state, emit, route, ship });
11616
- const floorStop = await checkCrewFloor({ state, emit, route, ship });
11617
- const itemStop = repoStopFor(openShipAttention(state), route);
11615
+ await checkRepoDisks({ state, emit, route: route2, ship });
11616
+ const floorStop = await checkCrewFloor({ state, emit, route: route2, ship });
11617
+ const itemStop = repoStopFor(openShipAttention(state), route2);
11618
11618
  const selectionInput = {
11619
11619
  repoStop: floorStop ? () => floorStop : itemStop,
11620
11620
  workModes,
@@ -11623,7 +11623,7 @@ async function decideCycle(o) {
11623
11623
  watermark,
11624
11624
  blocked,
11625
11625
  holds: holds2,
11626
- seats: route.resolved.seats,
11626
+ seats: route2.resolved.seats,
11627
11627
  paused: state.pausedRoles(["dev", "design", "qa"]),
11628
11628
  contract: tracker.contract,
11629
11629
  ships: { myShipId: shipIdByName(ships, ship.name), rows: ships }
@@ -11669,7 +11669,7 @@ async function decideCycle(o) {
11669
11669
  }
11670
11670
  return decision;
11671
11671
  }
11672
- var digestPath = (stateDir, route, role, runId) => join25(stateDir, `digest-${route}-${role}${runId ? `-${runId}` : ""}.md`);
11672
+ var digestPath = (stateDir, route2, role, runId) => join25(stateDir, `digest-${route2}-${role}${runId ? `-${runId}` : ""}.md`);
11673
11673
  function otherRunsRemoved(tickets, own) {
11674
11674
  if (!own) return tickets;
11675
11675
  return tickets.filter((t) => t.id === own.ticket.id || !own.inFlight(t.id));
@@ -11757,8 +11757,8 @@ ${body}` : body;
11757
11757
  return false;
11758
11758
  }
11759
11759
  }
11760
- function rosterFor(d, route, role) {
11761
- return rosterMarkdown(d.roster, route.resolved.seats[role] ?? null);
11760
+ function rosterFor(d, route2, role) {
11761
+ return rosterMarkdown(d.roster, route2.resolved.seats[role] ?? null);
11762
11762
  }
11763
11763
 
11764
11764
  // src/claim.ts
@@ -11787,11 +11787,11 @@ async function stampResumption(tracker, t, affinity) {
11787
11787
  }
11788
11788
  return { outcome: "unknown" };
11789
11789
  }
11790
- async function resolveTopCandidate(tracker, candidates, seat, approvedStatus, buildingStatus, isServable, affinity, ticketLock) {
11790
+ async function resolveTopCandidate(tracker, candidates2, seat, approvedStatus, buildingStatus, isServable, affinity, ticketLock) {
11791
11791
  const contended = [];
11792
11792
  const unservable = [];
11793
11793
  const held = [];
11794
- for (const t of candidates) {
11794
+ for (const t of candidates2) {
11795
11795
  if (affinity?.isHeldElsewhere?.(t)) {
11796
11796
  held.push(t.issue_id);
11797
11797
  continue;
@@ -12047,9 +12047,9 @@ function failingPaths(output, cwd) {
12047
12047
  }
12048
12048
  return [...found];
12049
12049
  }
12050
- function culpritByFiles(candidates, paths) {
12050
+ function culpritByFiles(candidates2, paths) {
12051
12051
  if (paths.length === 0) return null;
12052
- const hits = candidates.filter((c) => paths.some((p) => c.changed.has(p)));
12052
+ const hits = candidates2.filter((c) => paths.some((p) => c.changed.has(p)));
12053
12053
  return hits.length === 1 ? hits[0] : null;
12054
12054
  }
12055
12055
  function planBuildFailureBounce(cwd, c, base, remote) {
@@ -12942,9 +12942,9 @@ ${h.prFailure.tail}
12942
12942
  function emitReleaseSummary(emit, summaries) {
12943
12943
  if (summaries.length <= 1) return;
12944
12944
  const routes = [...new Set(summaries.map((s) => s.route))];
12945
- for (const route of routes) {
12946
- const rows = summaries.filter((s) => s.route === route);
12947
- const remit = emit.forRoute(route);
12945
+ for (const route2 of routes) {
12946
+ const rows = summaries.filter((s) => s.route === route2);
12947
+ const remit = emit.forRoute(route2);
12948
12948
  remit.enter("release");
12949
12949
  remit.emit(`release summary \u2014 ${rows.length} repo(s):`);
12950
12950
  for (const s of rows) {
@@ -13024,7 +13024,7 @@ var hook = async (o, name, env = {}) => {
13024
13024
  }
13025
13025
  return result;
13026
13026
  };
13027
- async function detectExternalClosures(o, candidates) {
13027
+ async function detectExternalClosures(o, candidates2) {
13028
13028
  const script = o.repo.hooks.merged;
13029
13029
  const mergedHook = script ? async (env) => (await runScript(script, {
13030
13030
  cwd: o.cwd,
@@ -13035,7 +13035,7 @@ async function detectExternalClosures(o, candidates) {
13035
13035
  }
13036
13036
  })).code : void 0;
13037
13037
  const out = [];
13038
- for (const c of candidates) {
13038
+ for (const c of candidates2) {
13039
13039
  if (!c.branch) continue;
13040
13040
  const prRef = c.ticket[o.contract.columns.prRef];
13041
13041
  const closure = await detectClosure({
@@ -13913,14 +13913,14 @@ var FAILURE_ALERT_MARKER = calloutHeader("CAUTION", "Failure Alert");
13913
13913
  function findOpenFailureAlert(tickets, headline) {
13914
13914
  return tickets.find((t) => t.title === headline);
13915
13915
  }
13916
- function ticketBody(n, route) {
13916
+ function ticketBody(n, route2) {
13917
13917
  return `${FAILURE_ALERT_MARKER}
13918
13918
 
13919
13919
  ${n.detail ?? n.headline}
13920
13920
 
13921
- Filed automatically \u2014 no \`notify\` hook is configured for route \`${route}\`, so this is the crew's built-in fallback rather than a silent log line. Configure \`hooks.notify\` on this route to replace it.`;
13921
+ Filed automatically \u2014 no \`notify\` hook is configured for route \`${route2}\`, so this is the crew's built-in fallback rather than a silent log line. Configure \`hooks.notify\` on this route to replace it.`;
13922
13922
  }
13923
- async function applyFailureAlert(writer, n, route, openTickets, memberId, log, dryRun, placement = {}) {
13923
+ async function applyFailureAlert(writer, n, route2, openTickets, memberId, log, dryRun, placement = {}) {
13924
13924
  const existing = findOpenFailureAlert(openTickets, n.headline);
13925
13925
  if (existing) {
13926
13926
  const label = displayKey(existing);
@@ -13951,7 +13951,7 @@ Still failing: ${body}`, memberId, "failure_alert");
13951
13951
  try {
13952
13952
  const created = await writer.fileTicket({
13953
13953
  title: n.headline,
13954
- description: ticketBody(n, route),
13954
+ description: ticketBody(n, route2),
13955
13955
  report_type: "bug",
13956
13956
  severity: "s2",
13957
13957
  reporter_name: "Crew release",
@@ -14153,15 +14153,15 @@ async function settleRunEnd(o) {
14153
14153
  emit.warn(`could not beat ship idle: ${e.message}`, { step: "agent" });
14154
14154
  }
14155
14155
  }
14156
- function describeRun(run2, route, seatIds, worktreeOf = (d, n) => {
14156
+ function describeRun(run2, route2, seatIds, worktreeOf = (d, n) => {
14157
14157
  try {
14158
14158
  return worktreeForNumber(d, n)?.path ?? null;
14159
14159
  } catch {
14160
14160
  return null;
14161
14161
  }
14162
14162
  }) {
14163
- const ownRoute = run2.route === routeSlug(route.route);
14164
- const repoDir = run2.repo ? reposOf(route).find((t) => t.name === run2.repo)?.dir : void 0;
14163
+ const ownRoute = run2.route === routeSlug(route2.route);
14164
+ const repoDir = run2.repo ? reposOf(route2).find((t) => t.name === run2.repo)?.dir : void 0;
14165
14165
  const num = run2.ticket?.replace(/^\D+/, "") ?? "";
14166
14166
  return {
14167
14167
  role: run2.role,
@@ -14282,29 +14282,29 @@ function runForge(a, io) {
14282
14282
  // src/whoami.ts
14283
14283
  async function buildWhoami(routes, deps = {}) {
14284
14284
  const doFetch = deps.fetch ?? fetch;
14285
- return Promise.all(routes.map(async (route) => {
14286
- const credential = credentialSource(route);
14285
+ return Promise.all(routes.map(async (route2) => {
14286
+ const credential = credentialSource(route2);
14287
14287
  try {
14288
- const key = resolveApiKey(route);
14289
- const q = route.resolved ? `?workspaceId=${encodeURIComponent(route.resolved.workspaceId)}` : "";
14290
- const res = await doFetch(`${route.baseUrl.replace(/\/+$/, "")}/api/auth/me${q}`, {
14288
+ const key = resolveApiKey(route2);
14289
+ const q = route2.resolved ? `?workspaceId=${encodeURIComponent(route2.resolved.workspaceId)}` : "";
14290
+ const res = await doFetch(`${route2.baseUrl.replace(/\/+$/, "")}/api/auth/me${q}`, {
14291
14291
  headers: { Authorization: `Bearer ${key}`, "User-Agent": deps.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
14292
14292
  });
14293
14293
  if (res.status === 401 || res.status === 403) {
14294
- return { route: route.route, ok: false, credential, error: `key rejected by ${route.baseUrl} (HTTP ${res.status}): expired or revoked, re-run crew connect` };
14294
+ return { route: route2.route, ok: false, credential, error: `key rejected by ${route2.baseUrl} (HTTP ${res.status}): expired or revoked, re-run crew connect` };
14295
14295
  }
14296
- if (!res.ok) return { route: route.route, ok: false, credential, error: `/auth/me answered HTTP ${res.status}` };
14296
+ if (!res.ok) return { route: route2.route, ok: false, credential, error: `/auth/me answered HTTP ${res.status}` };
14297
14297
  const me = await res.json();
14298
- const operatorId = route.resolved?.operator ?? null;
14298
+ const operatorId = route2.resolved?.operator ?? null;
14299
14299
  let operatorName = null;
14300
14300
  if (operatorId && deps.operatorName) {
14301
14301
  try {
14302
- operatorName = await deps.operatorName(route, operatorId) ?? null;
14302
+ operatorName = await deps.operatorName(route2, operatorId) ?? null;
14303
14303
  } catch {
14304
14304
  }
14305
14305
  }
14306
14306
  return {
14307
- route: route.route,
14307
+ route: route2.route,
14308
14308
  ok: true,
14309
14309
  credential,
14310
14310
  identity: { id: me.id ?? null, email: me.email ?? null, name: me.name ?? null },
@@ -14312,7 +14312,7 @@ async function buildWhoami(routes, deps = {}) {
14312
14312
  operator: { id: operatorId, name: operatorName }
14313
14313
  };
14314
14314
  } catch (e) {
14315
- return { route: route.route, ok: false, credential, error: e.message };
14315
+ return { route: route2.route, ok: false, credential, error: e.message };
14316
14316
  }
14317
14317
  }));
14318
14318
  }
@@ -14336,10 +14336,10 @@ function formatWhoami(report) {
14336
14336
 
14337
14337
  // src/seat-read-probe.ts
14338
14338
  var SEAT_NO_READ_REMEDY = "the project has no default role for agent keys \u2014 update the Issues app's template, then re-mint with `crew agents sync --revoke-seat-keys`, or assign the Crew seat role to the agent members";
14339
- function probeTables(route) {
14340
- const m = route.resolved?.models;
14339
+ function probeTables(route2) {
14340
+ const m = route2.resolved?.models;
14341
14341
  if (!m) return [];
14342
- const r = route.resolved;
14342
+ const r = route2.resolved;
14343
14343
  return [
14344
14344
  { name: "Issues", modelId: m.issues },
14345
14345
  { name: "Comments", modelId: m.comments },
@@ -14347,8 +14347,8 @@ function probeTables(route) {
14347
14347
  { name: "Ships", modelId: r?.shipsModelId ?? "" }
14348
14348
  ].filter((t) => !!t.modelId);
14349
14349
  }
14350
- async function probeTableRead(route, modelId, apiKey, o = {}) {
14351
- const url = `${route.baseUrl.replace(/\/+$/, "")}/api/data-models/${modelId}/records?limit=1`;
14350
+ async function probeTableRead(route2, modelId, apiKey, o = {}) {
14351
+ const url = `${route2.baseUrl.replace(/\/+$/, "")}/api/data-models/${modelId}/records?limit=1`;
14352
14352
  try {
14353
14353
  const res = await (o.fetchImpl ?? fetch)(url, {
14354
14354
  headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": o.userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
@@ -14360,25 +14360,25 @@ async function probeTableRead(route, modelId, apiKey, o = {}) {
14360
14360
  return { state: "unknown", detail: e.message };
14361
14361
  }
14362
14362
  }
14363
- async function probeSeatReads(route, o = {}) {
14363
+ async function probeSeatReads(route2, o = {}) {
14364
14364
  const out = [];
14365
- const roles = [...SEAT_KEY_ROLES.filter((r) => route.resolved?.seats[r]), ...route.resolved?.releaseSeat ? ["release"] : []];
14365
+ const roles = [...SEAT_KEY_ROLES.filter((r) => route2.resolved?.seats[r]), ...route2.resolved?.releaseSeat ? ["release"] : []];
14366
14366
  for (const role of roles) {
14367
- const id = role === "release" ? await resolveReleaseKey(route, o.getStore) : await resolveSeatKey(route, role, o.getStore);
14367
+ const id = role === "release" ? await resolveReleaseKey(route2, o.getStore) : await resolveSeatKey(route2, role, o.getStore);
14368
14368
  if (id.kind !== "agent") continue;
14369
14369
  const tables = [];
14370
- for (const t of probeTables(route)) tables.push({ table: t.name, ...await probeTableRead(route, t.modelId, id.apiKey, o) });
14370
+ for (const t of probeTables(route2)) tables.push({ table: t.name, ...await probeTableRead(route2, t.modelId, id.apiKey, o) });
14371
14371
  const bad = tables.find((t) => t.state === "forbidden") ?? tables.find((t) => t.state === "unknown");
14372
14372
  out.push({ role, state: bad?.state ?? (tables.length ? "ok" : "unknown"), detail: bad?.detail ?? (tables.length ? void 0 : "route has no resolved tables"), tables });
14373
14373
  }
14374
14374
  return out;
14375
14375
  }
14376
- function forbiddenDetail(e, route) {
14376
+ function forbiddenDetail(e, route2) {
14377
14377
  const msg = e instanceof Error ? e.message : String(e);
14378
14378
  const m = /^([A-Z]+) (\S+) -> 403/.exec(msg);
14379
14379
  const verb = /No (READ|WRITE) access/i.exec(msg)?.[1]?.toUpperCase();
14380
14380
  const modelId = /\/data-models\/([\w-]+)/i.exec(m?.[2] ?? "")?.[1];
14381
- const r = route?.resolved;
14381
+ const r = route2?.resolved;
14382
14382
  const known = [
14383
14383
  [r?.models?.issues, "Issues"],
14384
14384
  [r?.models?.comments, "Comments"],
@@ -14414,8 +14414,8 @@ function isForbiddenError(e) {
14414
14414
  return /\b403\b/.test(e instanceof Error ? e.message : String(e));
14415
14415
  }
14416
14416
  var SEAT_FORBIDDEN_KEY = "seat_forbidden:release";
14417
- function seatForbiddenItem(now = /* @__PURE__ */ new Date(), e, route) {
14418
- const d = e === void 0 ? {} : forbiddenDetail(e, route);
14417
+ function seatForbiddenItem(now = /* @__PURE__ */ new Date(), e, route2) {
14418
+ const d = e === void 0 ? {} : forbiddenDetail(e, route2);
14419
14419
  const what = d.method && d.table ? `${d.method} ${d.table}${d.verb ? ` -> 403 No ${d.verb} access` : " -> 403"}` : "cannot read the Issues table (403)";
14420
14420
  return {
14421
14421
  kind: "seat_forbidden",
@@ -14461,8 +14461,8 @@ var red = paintWith(31);
14461
14461
  var cyan = paintWith(36);
14462
14462
  var STEP_ORDER = ["poll", "sweep", "select", "worktree", "agent", "reconcile", "merge", "release", "idle"];
14463
14463
  var WORKING_STEPS = /* @__PURE__ */ new Set(["worktree", "agent", "reconcile", "merge", "release"]);
14464
- function byRoute(events, route) {
14465
- return route ? events.filter((e) => e.route === route) : events;
14464
+ function byRoute(events, route2) {
14465
+ return route2 ? events.filter((e) => e.route === route2) : events;
14466
14466
  }
14467
14467
  function matches(e, f) {
14468
14468
  if (f.role && e.role !== f.role) return false;
@@ -14482,9 +14482,9 @@ function formatLine(e, showRoute = false) {
14482
14482
  const t = dim(localTime(e.at));
14483
14483
  const scope = e.role ? `${e.step}[${e.role}]` : e.step;
14484
14484
  const paint = e.level === "error" ? red : e.level === "warn" ? yellow : cyan;
14485
- const route = showRoute ? `${dim(e.route)} ` : "";
14485
+ const route2 = showRoute ? `${dim(e.route)} ` : "";
14486
14486
  const ticket = e.ticket ? ` ${bold(e.ticket)}` : "";
14487
- return `${t} ${route}${paint(scope.padEnd(14))}${ticket} ${e.message}`;
14487
+ return `${t} ${route2}${paint(scope.padEnd(14))}${ticket} ${e.message}`;
14488
14488
  }
14489
14489
  function foldCycle(events) {
14490
14490
  const last = events[events.length - 1];
@@ -14640,23 +14640,23 @@ async function gatherInbox(routes, ship, opts = {}) {
14640
14640
  const items = [];
14641
14641
  const errors = [];
14642
14642
  const resolved = [];
14643
- await Promise.all(routes.map(async (route) => {
14643
+ await Promise.all(routes.map(async (route2) => {
14644
14644
  try {
14645
- const tracker = new Tracker(route, ship);
14645
+ const tracker = new Tracker(route2, ship);
14646
14646
  const [tickets, crewRows] = await Promise.all([tracker.openTickets(), tracker.crewRows()]);
14647
- const roster = buildRoster(configuredMembers(route), crewRows);
14647
+ const roster = buildRoster(configuredMembers(route2), crewRows);
14648
14648
  let mine;
14649
14649
  if (opts.memberQuery) {
14650
14650
  const hits = matchMembers(crewRows, opts.memberQuery);
14651
14651
  resolved.push({
14652
- route: route.route,
14652
+ route: route2.route,
14653
14653
  names: hits.map((h) => h.name),
14654
14654
  emails: hits.map((h) => h.email).filter(Boolean),
14655
14655
  by: hits[0]?.by ?? null
14656
14656
  });
14657
14657
  mine = new Set(hits.map((h) => h.id));
14658
14658
  } else {
14659
- mine = opts.memberIds ?? new Set([route.resolved?.operator].filter(Boolean));
14659
+ mine = opts.memberIds ?? new Set([route2.resolved?.operator].filter(Boolean));
14660
14660
  }
14661
14661
  const c = tracker.contract;
14662
14662
  for (const t of tickets) {
@@ -14664,7 +14664,7 @@ async function gatherInbox(routes, ship, opts = {}) {
14664
14664
  if (opts.statuses && !opts.statuses.has(t.status)) continue;
14665
14665
  if (!opts.includeClosed && c.statuses.resolved.includes(t.status)) continue;
14666
14666
  items.push({
14667
- route: route.route,
14667
+ route: route2.route,
14668
14668
  ticket: t,
14669
14669
  assignee: crewLabel(roster.get(t.assignee_id)),
14670
14670
  rank: rankScalar(t, c),
@@ -14673,7 +14673,7 @@ async function gatherInbox(routes, ship, opts = {}) {
14673
14673
  });
14674
14674
  }
14675
14675
  } catch (e) {
14676
- errors.push({ route: route.route, error: e.message });
14676
+ errors.push({ route: route2.route, error: e.message });
14677
14677
  }
14678
14678
  }));
14679
14679
  items.sort((a, b) => a.rank - b.rank);
@@ -14708,29 +14708,29 @@ function renderInbox(items, byConnection = false, subject) {
14708
14708
  }
14709
14709
 
14710
14710
  // src/fleet.ts
14711
- function agedConnectionRank(rawRank, role, route, decision, state, now) {
14711
+ function agedConnectionRank(rawRank, role, route2, decision, state, now) {
14712
14712
  if (role !== "dev" && role !== "design") return rawRank;
14713
- const waiting = state.fairness(route.route).waiting();
14713
+ const waiting = state.fairness(route2.route).waiting();
14714
14714
  if (!waiting || waiting.ticket !== decision.actionable.top?.issue_id) return rawRank;
14715
- return agedRankScalar(rawRank, { since: waiting.since, weight: route.weight }, now);
14715
+ return agedRankScalar(rawRank, { since: waiting.since, weight: route2.weight }, now);
14716
14716
  }
14717
14717
  async function decideFleet(o) {
14718
14718
  const now = o.now ?? Date.now();
14719
14719
  const eligible = o.routes.filter((c) => o.enabledOnly === false ? true : c.enabled);
14720
- const entries = await Promise.all(eligible.map(async (route) => {
14720
+ const entries = await Promise.all(eligible.map(async (route2) => {
14721
14721
  try {
14722
- const decision = await decideCycle({ route, ship: o.ship, state: o.state, emit: o.emit.forRoute(route.route) });
14722
+ const decision = await decideCycle({ route: route2, ship: o.ship, state: o.state, emit: o.emit.forRoute(route2.route) });
14723
14723
  const sel = decision.selection;
14724
- if (!sel.selected) return { route, decision, rank: NOTHING_ACTIONABLE };
14724
+ if (!sel.selected) return { route: route2, decision, rank: NOTHING_ACTIONABLE };
14725
14725
  const rawRank = sel.ranks[sel.selected] ?? NOTHING_ACTIONABLE;
14726
14726
  return {
14727
- route,
14727
+ route: route2,
14728
14728
  decision,
14729
14729
  role: sel.selected,
14730
- rank: agedConnectionRank(rawRank, sel.selected, route, decision, o.state, now)
14730
+ rank: agedConnectionRank(rawRank, sel.selected, route2, decision, o.state, now)
14731
14731
  };
14732
14732
  } catch (e) {
14733
- return { route, rank: NOTHING_ACTIONABLE, error: e.message };
14733
+ return { route: route2, rank: NOTHING_ACTIONABLE, error: e.message };
14734
14734
  }
14735
14735
  }));
14736
14736
  for (const e of entries) {
@@ -15062,8 +15062,8 @@ async function del(o, path) {
15062
15062
  });
15063
15063
  if (!res.ok) throw new ConnectHttpError(res.status, path, res.statusText);
15064
15064
  }
15065
- async function provisionReleaseLocks(o, locksModelId, route, repoNames) {
15066
- const scopes = [...new Set(repoNames)].sort().map((r) => releaseLockScope(route, r));
15065
+ async function provisionReleaseLocks(o, locksModelId, route2, repoNames) {
15066
+ const scopes = [...new Set(repoNames)].sort().map((r) => releaseLockScope(route2, r));
15067
15067
  if (scopes.length === 0) return [];
15068
15068
  const client2 = {
15069
15069
  // The Locks table is a handful of rows, so read it whole and match in
@@ -15235,14 +15235,14 @@ async function discover(o) {
15235
15235
  out.locksModelId = find("Locks");
15236
15236
  try {
15237
15237
  const views = await get(o, `/views?workspaceId=${ws.id}`);
15238
- const candidates = views.filter((v) => v.recordLinkOnly === true && v.published === true && !!v.publicSlug && (v.pages ?? []).some((p) => (p.components ?? []).some((c) => c.dataModelId === out.models.issues)));
15239
- if (candidates.length === 1) {
15240
- out.recordLinkViewId = candidates[0].id;
15241
- } else if (candidates.length === 0) {
15238
+ const candidates2 = views.filter((v) => v.recordLinkOnly === true && v.published === true && !!v.publicSlug && (v.pages ?? []).some((p) => (p.components ?? []).some((c) => c.dataModelId === out.models.issues)));
15239
+ if (candidates2.length === 1) {
15240
+ out.recordLinkViewId = candidates2[0].id;
15241
+ } else if (candidates2.length === 0) {
15242
15242
  problems.push("no record-link-only view bound to Issues \u2014 attention events will carry no link");
15243
15243
  } else {
15244
15244
  problems.push(
15245
- `${candidates.length} record-link-only views bound to Issues \u2014 set resolved.recordLinkViewId by hand`
15245
+ `${candidates2.length} record-link-only views bound to Issues \u2014 set resolved.recordLinkViewId by hand`
15246
15246
  );
15247
15247
  }
15248
15248
  } catch {
@@ -15312,8 +15312,8 @@ async function discover(o) {
15312
15312
  const patchBody = {};
15313
15313
  if (matchedBy === "machine_id") {
15314
15314
  const rowName = (mine[0].name ?? "").trim();
15315
- const route = `${out.workspaceSlug}/${out.projectSlug ?? out.projectId}`;
15316
- const plan = planNameSync(o.ship.name.trim(), rowName, o.ship.lastSyncedName?.(route));
15315
+ const route2 = `${out.workspaceSlug}/${out.projectSlug ?? out.projectId}`;
15316
+ const plan = planNameSync(o.ship.name.trim(), rowName, o.ship.lastSyncedName?.(route2));
15317
15317
  if (plan.action === "push") {
15318
15318
  patchBody.name = o.ship.name.trim();
15319
15319
  provisioning.push(`renamed Ships row "${rowName}" to "${o.ship.name.trim()}" (crew.yaml changed)`);
@@ -15477,11 +15477,11 @@ async function discover(o) {
15477
15477
  else if (out.candidates.length === 1) out.operator = out.candidates[0].id;
15478
15478
  }
15479
15479
  } else {
15480
- const candidates = people.filter((r) => !r.user_id || isMine(r));
15480
+ const candidates2 = people.filter((r) => !r.user_id || isMine(r));
15481
15481
  const mine = people.find(isMine);
15482
- const byEmail = out.meEmail ? candidates.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
15482
+ const byEmail = out.meEmail ? candidates2.filter((r) => emailOf(r).some((e) => eq(e, out.meEmail))) : [];
15483
15483
  const unlinked = people.filter((r) => !r.user_id);
15484
- const only = candidates.length === 1 && unlinked.length === 1 ? candidates[0] : void 0;
15484
+ const only = candidates2.length === 1 && unlinked.length === 1 ? candidates2[0] : void 0;
15485
15485
  const pick = mine ?? (byEmail.length === 1 ? byEmail[0] : only);
15486
15486
  if (pick) {
15487
15487
  out.operator = pick.id;
@@ -15492,7 +15492,7 @@ async function discover(o) {
15492
15492
  const m = r.user_id ? members.get(r.user_id) : void 0;
15493
15493
  return `"${r.name || "unnamed row"}" <${r.email ?? "no email"}>` + (r.user_id ? ` linked to member ${m?.email ?? r.user_id}${m?.identityId ? ` (identity ${m.identityId})` : ""}` : " unlinked");
15494
15494
  }).join("; "));
15495
- if (o.ship && (!o.interactive || candidates.length === 0)) await createOperatorRow(o, out);
15495
+ if (o.ship && (!o.interactive || candidates2.length === 0)) await createOperatorRow(o, out);
15496
15496
  }
15497
15497
  }
15498
15498
  }
@@ -15541,7 +15541,7 @@ async function createOperatorRow(o, found) {
15541
15541
  found.provisioning.push("could not create your Crew row (no write access) \u2014 ask a workspace admin to add one with your user_id");
15542
15542
  }
15543
15543
  }
15544
- function renderConnection(d, route, dir, opts = {}) {
15544
+ function renderConnection(d, route2, dir, opts = {}) {
15545
15545
  const apiKeyFileLine = opts.keychainBacked ? "" : opts.apiKeyFile ? ` apiKeyFile: "${opts.apiKeyFile}"` : ` apiKeyFile: "REPLACE \u2014 a file holding this workspace's key"`;
15546
15546
  const apiKeyVarLine = opts.keychainBacked ? "" : opts.apiKeyVar ? `
15547
15547
  apiKeyVar: ${opts.apiKeyVar}` : `
@@ -15549,7 +15549,7 @@ function renderConnection(d, route, dir, opts = {}) {
15549
15549
  const baseUrlLine = opts.baseUrl ? opts.baseUrl.replace(/\/+$/, "") === DEFAULT_BASE_URL ? "" : ` baseUrl: "${opts.baseUrl}"
15550
15550
  ` : ' baseUrl: "REPLACE \u2014 the same baseUrl as your other routes"\n';
15551
15551
  const keyLines = [apiKeyFileLine, apiKeyVarLine.replace(/^\n/, "")].filter(Boolean).join("\n");
15552
- return ` - route: ${route}
15552
+ return ` - route: ${route2}
15553
15553
  enabled: false # set to true once \`crew doctor\` is green
15554
15554
  ${opts.area ? ` area: "${opts.area}"
15555
15555
  ` : ""}${dir === null ? "" : ` dir: "${dir}"
@@ -15668,26 +15668,26 @@ import { createHash as createHash5 } from "crypto";
15668
15668
  import { TablationClient as TablationClient4 } from "@tablation/client";
15669
15669
  var LogbookError = class extends Error {
15670
15670
  };
15671
- function client(route, userAgent) {
15671
+ function client(route2, userAgent) {
15672
15672
  return new TablationClient4({
15673
- baseUrl: `${route.baseUrl}/api`,
15674
- apiKey: resolveApiKey(route),
15673
+ baseUrl: `${route2.baseUrl}/api`,
15674
+ apiKey: resolveApiKey(route2),
15675
15675
  headers: { "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
15676
15676
  });
15677
15677
  }
15678
- function requireWorkspace(route) {
15679
- if (!route.resolved) {
15680
- throw new LogbookError(`route "${route.route}" has no resolved ids \u2014 run \`crew connect\` first`);
15678
+ function requireWorkspace(route2) {
15679
+ if (!route2.resolved) {
15680
+ throw new LogbookError(`route "${route2.route}" has no resolved ids \u2014 run \`crew connect\` first`);
15681
15681
  }
15682
15682
  }
15683
- async function listLogEntries(route, opts = {}) {
15684
- requireWorkspace(route);
15685
- const c = client(route, opts.userAgent);
15686
- const agentLogModel = await c.dataModels.get("agent_log", route.resolved.workspaceId);
15683
+ async function listLogEntries(route2, opts = {}) {
15684
+ requireWorkspace(route2);
15685
+ const c = client(route2, opts.userAgent);
15686
+ const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
15687
15687
  const filters = [];
15688
15688
  if (opts.ticket) filters.push({ columnName: "ticket_reference", operator: "EQ", value: opts.ticket });
15689
15689
  if (opts.role) {
15690
- const agentsModel = await c.dataModels.get("agents", route.resolved.workspaceId);
15690
+ const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
15691
15691
  const rows = await c.records.list(agentsModel.id, { limit: 200 });
15692
15692
  const match = rows.find((r) => r.name.toLowerCase() === opts.role.toLowerCase());
15693
15693
  if (!match) return [];
@@ -15699,10 +15699,10 @@ async function listLogEntries(route, opts = {}) {
15699
15699
  });
15700
15700
  return [...entries].sort((a, b) => String(b.started_at ?? "").localeCompare(String(a.started_at ?? "")));
15701
15701
  }
15702
- async function showLogEntry(route, entryId, opts = {}) {
15703
- requireWorkspace(route);
15704
- const c = client(route, opts.userAgent);
15705
- const agentLogModel = await c.dataModels.get("agent_log", route.resolved.workspaceId);
15702
+ async function showLogEntry(route2, entryId, opts = {}) {
15703
+ requireWorkspace(route2);
15704
+ const c = client(route2, opts.userAgent);
15705
+ const agentLogModel = await c.dataModels.get("agent_log", route2.resolved.workspaceId);
15706
15706
  let entry;
15707
15707
  try {
15708
15708
  entry = await c.records.get(agentLogModel.id, entryId);
@@ -15719,10 +15719,10 @@ async function showLogEntry(route, entryId, opts = {}) {
15719
15719
  }
15720
15720
  };
15721
15721
  }
15722
- const agentsModel = await c.dataModels.get("agents", route.resolved.workspaceId);
15722
+ const agentsModel = await c.dataModels.get("agents", route2.resolved.workspaceId);
15723
15723
  let historyEntries;
15724
15724
  try {
15725
- historyEntries = await fetchHistoryEntries(route, agentsModel.id, entry.agent_id, opts.userAgent, 200);
15725
+ historyEntries = await fetchHistoryEntries(route2, agentsModel.id, entry.agent_id, opts.userAgent, 200);
15726
15726
  } catch (e) {
15727
15727
  return { entry, reconstruction: { status: "approximate", reason: `history lookup failed: ${e.message}` } };
15728
15728
  }
@@ -15882,37 +15882,37 @@ function routeNamesOf(text) {
15882
15882
  }
15883
15883
  function parseAddArgs(args, routes, isDir) {
15884
15884
  const rest = [...args];
15885
- let route;
15886
- if (rest.length > 0 && routes.includes(rest[0])) route = rest.shift();
15885
+ let route2;
15886
+ if (rest.length > 0 && routes.includes(rest[0])) route2 = rest.shift();
15887
15887
  else if (rest.length > 0 && /^[^/\s]+\/[^/\s]+$/.test(rest[0]) && !isDir(rest[0])) {
15888
15888
  throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
15889
15889
  }
15890
- if (!route) {
15891
- if (routes.length === 1) route = routes[0];
15890
+ if (!route2) {
15891
+ if (routes.length === 1) route2 = routes[0];
15892
15892
  else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
15893
15893
  }
15894
15894
  if (rest.length > 2) throw new ReposError("too many arguments: expected [route] [name] [path]");
15895
- if (rest.length === 2) return { route, name: rest[0], path: rest[1] };
15896
- if (rest.length === 1) return isDir(rest[0]) ? { route, path: rest[0] } : { route, name: rest[0], path: "." };
15897
- return { route, path: "." };
15895
+ if (rest.length === 2) return { route: route2, name: rest[0], path: rest[1] };
15896
+ if (rest.length === 1) return isDir(rest[0]) ? { route: route2, path: rest[0] } : { route: route2, name: rest[0], path: "." };
15897
+ return { route: route2, path: "." };
15898
15898
  }
15899
15899
  function expandDir(p, base) {
15900
15900
  const home = p === "~" ? homedir9() : p.startsWith("~/") ? `${homedir9()}/${p.slice(2)}` : p;
15901
15901
  return resolve3(base, home);
15902
15902
  }
15903
- function routeMapOf(doc, route) {
15903
+ function routeMapOf(doc, route2) {
15904
15904
  const routes = doc.get("routes", true);
15905
15905
  if (isSeq(routes)) {
15906
15906
  for (const item of routes.items) {
15907
- if (isMap(item) && item.get("route") === route) return item;
15907
+ if (isMap(item) && item.get("route") === route2) return item;
15908
15908
  }
15909
15909
  }
15910
15910
  const have = isSeq(routes) ? routes.items.map((i) => isMap(i) ? String(i.get("route")) : "?").join(", ") : "none";
15911
- throw new ReposError(`no route "${route}" in the config (have: ${have})`);
15911
+ throw new ReposError(`no route "${route2}" in the config (have: ${have})`);
15912
15912
  }
15913
- function configuredRepos(text, route, base) {
15913
+ function configuredRepos(text, route2, base) {
15914
15914
  const doc = parseDocument3(text);
15915
- const m = routeMapOf(doc, route);
15915
+ const m = routeMapOf(doc, route2);
15916
15916
  const repos = m.get("repos");
15917
15917
  if (isMap(repos)) {
15918
15918
  return repos.items.map((p) => {
@@ -15925,8 +15925,8 @@ function configuredRepos(text, route, base) {
15925
15925
  const dir = m.get("dir");
15926
15926
  return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
15927
15927
  }
15928
- function cloneBaseOf(text, route, base, isCheckout) {
15929
- const m = routeMapOf(parseDocument3(text), route);
15928
+ function cloneBaseOf(text, route2, base, isCheckout) {
15929
+ const m = routeMapOf(parseDocument3(text), route2);
15930
15930
  const repos = m.get("repos", true);
15931
15931
  if (isMap(repos) && repos.items.length > 0) return null;
15932
15932
  const dir = m.get("dir");
@@ -15934,8 +15934,8 @@ function cloneBaseOf(text, route, base, isCheckout) {
15934
15934
  const abs = expandDir(String(dir), base);
15935
15935
  return isCheckout(abs) ? null : abs;
15936
15936
  }
15937
- function routeSetsHooks(text, route, name) {
15938
- const m = routeMapOf(parseDocument3(text), route);
15937
+ function routeSetsHooks(text, route2, name) {
15938
+ const m = routeMapOf(parseDocument3(text), route2);
15939
15939
  const repos = m.get("repos", true);
15940
15940
  let target = m;
15941
15941
  if (isMap(repos)) target = repos.get(name, true);
@@ -16042,14 +16042,14 @@ function planRepoAdd(i) {
16042
16042
  }
16043
16043
  return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
16044
16044
  }
16045
- function planRepoRemove(text, base, route, path) {
16045
+ function planRepoRemove(text, base, route2, path) {
16046
16046
  const doc = parseDocument3(text);
16047
16047
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
16048
- const m = routeMapOf(doc, route);
16048
+ const m = routeMapOf(doc, route2);
16049
16049
  const target = expandDir(path, base);
16050
- const existing = configuredRepos(text, route, base);
16050
+ const existing = configuredRepos(text, route2, base);
16051
16051
  const hit = existing.find((e) => e.dir === target);
16052
- if (!hit) throw new ReposError(`${target} is not configured on route ${route}`);
16052
+ if (!hit) throw new ReposError(`${target} is not configured on route ${route2}`);
16053
16053
  const repos = m.get("repos", true);
16054
16054
  if (isMap(repos)) {
16055
16055
  repos.delete(hit.name);
@@ -16097,15 +16097,15 @@ function resolveRepoProject(projects, opt, routeAreaId) {
16097
16097
  }
16098
16098
  function parseCloneArgs(args, routes) {
16099
16099
  const rest = [...args];
16100
- let route;
16101
- if (rest.length > 1 && routes.includes(rest[0])) route = rest.shift();
16100
+ let route2;
16101
+ if (rest.length > 1 && routes.includes(rest[0])) route2 = rest.shift();
16102
16102
  else if (rest.length > 1) throw new ReposError(`no route "${rest[0]}" in the config (have: ${routes.join(", ") || "none"})`);
16103
- if (!route) {
16104
- if (routes.length === 1) route = routes[0];
16103
+ if (!route2) {
16104
+ if (routes.length === 1) route2 = routes[0];
16105
16105
  else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
16106
16106
  }
16107
16107
  if (rest.length !== 1) throw new ReposError("expected [route] <name> \u2014 the Repos row to clone (see `crew repos list`)");
16108
- return { route, name: rest[0] };
16108
+ return { route: route2, name: rest[0] };
16109
16109
  }
16110
16110
  function planRepoClone(i) {
16111
16111
  const names = i.resolved?.repoNames ?? {};
@@ -16257,27 +16257,27 @@ function parseHookOverrides(specs) {
16257
16257
  }
16258
16258
  function parseHooksArgs(args, routes) {
16259
16259
  const rest = [...args];
16260
- let route;
16261
- if (rest.length > 1 && routes.includes(rest[0])) route = rest.shift();
16262
- if (!route) {
16263
- if (routes.length === 1) route = routes[0];
16260
+ let route2;
16261
+ if (rest.length > 1 && routes.includes(rest[0])) route2 = rest.shift();
16262
+ if (!route2) {
16263
+ if (routes.length === 1) route2 = routes[0];
16264
16264
  else throw new ReposError(routes.length === 0 ? "the config has no routes \u2014 run `crew connect` first" : `several routes are configured; name one: ${routes.join(", ")}`);
16265
16265
  }
16266
16266
  if (rest.length !== 1) throw new ReposError("expected: crew repos hooks [route] <name> [--suggest] [--agent] [--write route|repo] [--recipes ID,ID] [--hook KEY=CMD] [--json]");
16267
- return { route, name: rest[0] };
16267
+ return { route: route2, name: rest[0] };
16268
16268
  }
16269
- function repoDirOf(text, base, route, name) {
16270
- const repos = configuredRepos(text, route, base);
16269
+ function repoDirOf(text, base, route2, name) {
16270
+ const repos = configuredRepos(text, route2, base);
16271
16271
  const hit = repos.find((r) => r.name === name);
16272
16272
  if (!hit) {
16273
- throw new ReposError(`route ${route} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
16273
+ throw new ReposError(`route ${route2} has no repo named "${name}" (have: ${repos.map((r) => r.name).join(", ") || "none"})`);
16274
16274
  }
16275
16275
  return hit.dir;
16276
16276
  }
16277
- function planRouteHooks(text, route, name, hooks, opts = {}) {
16277
+ function planRouteHooks(text, route2, name, hooks, opts = {}) {
16278
16278
  const doc = parseDocument4(text);
16279
16279
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
16280
- const m = routeMapOf(doc, route);
16280
+ const m = routeMapOf(doc, route2);
16281
16281
  const repos = m.get("repos", true);
16282
16282
  let target = m;
16283
16283
  if (isMap2(repos)) {
@@ -16311,10 +16311,10 @@ function planRouteHooks(text, route, name, hooks, opts = {}) {
16311
16311
  }
16312
16312
  return { text: String(doc), written, kept, replaced };
16313
16313
  }
16314
- function clearRouteHooks(text, route, name) {
16314
+ function clearRouteHooks(text, route2, name) {
16315
16315
  const doc = parseDocument4(text);
16316
16316
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
16317
- const m = routeMapOf(doc, route);
16317
+ const m = routeMapOf(doc, route2);
16318
16318
  const repos = m.get("repos", true);
16319
16319
  let target = m;
16320
16320
  if (isMap2(repos)) {
@@ -16476,7 +16476,7 @@ async function runAgentSuggest(o) {
16476
16476
  const signal = o.signal ? AbortSignal.any([o.signal, timeout]) : timeout;
16477
16477
  const run2 = await new Promise((resolve6) => {
16478
16478
  let out = "";
16479
- let err = "";
16479
+ let err2 = "";
16480
16480
  const child = spawn4(bin, args, { cwd: o.dir, env, detached: true, stdio: ["pipe", "pipe", "pipe"] });
16481
16481
  let settled = false;
16482
16482
  const settle = (r) => {
@@ -16493,7 +16493,7 @@ async function runAgentSuggest(o) {
16493
16493
  }
16494
16494
  child.stdout.destroy();
16495
16495
  child.stderr.destroy();
16496
- settle({ code: null, out, err, aborted: true });
16496
+ settle({ code: null, out, err: err2, aborted: true });
16497
16497
  };
16498
16498
  if (signal.aborted) onAbort();
16499
16499
  else signal.addEventListener("abort", onAbort, { once: true });
@@ -16501,10 +16501,10 @@ async function runAgentSuggest(o) {
16501
16501
  out += d.toString();
16502
16502
  });
16503
16503
  child.stderr.on("data", (d) => {
16504
- err += d.toString();
16504
+ err2 += d.toString();
16505
16505
  });
16506
- child.on("error", (e) => settle({ code: null, out, err, spawnError: e, aborted: false }));
16507
- child.on("close", (code) => settle({ code, out, err, aborted: false }));
16506
+ child.on("error", (e) => settle({ code: null, out, err: err2, spawnError: e, aborted: false }));
16507
+ child.on("close", (code) => settle({ code, out, err: err2, aborted: false }));
16508
16508
  child.stdin.on("error", () => {
16509
16509
  });
16510
16510
  child.stdin.end(buildAgentPrompt(o.sniff));
@@ -16633,9 +16633,9 @@ async function gatherHealth(c, check) {
16633
16633
  function routesInScope(routes, named) {
16634
16634
  return named ? routes.filter((r) => r.route === named) : routes;
16635
16635
  }
16636
- function planEnable(text, route) {
16636
+ function planEnable(text, route2) {
16637
16637
  const doc = parseDocument5(text);
16638
- routeMapOf(doc, route).set("enabled", true);
16638
+ routeMapOf(doc, route2).set("enabled", true);
16639
16639
  return String(doc);
16640
16640
  }
16641
16641
  function schedulerInstalled(plan, exists, crontab) {
@@ -16975,7 +16975,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
16975
16975
  }
16976
16976
 
16977
16977
  // src/cli.ts
16978
- import { realpathSync as realpathSync3, readFileSync as readFileSync34, existsSync as existsSync38, mkdirSync as mkdirSync25, writeFileSync as writeFileSync26, rmSync as rmSync17, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync12, readdirSync as readdirSync17 } from "fs";
16978
+ import { realpathSync as realpathSync3, readFileSync as readFileSync34, existsSync as existsSync39, mkdirSync as mkdirSync25, writeFileSync as writeFileSync26, rmSync as rmSync18, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync13, readdirSync as readdirSync18 } from "fs";
16979
16979
  import { createInterface } from "readline/promises";
16980
16980
 
16981
16981
  // src/periscope-cmd.ts
@@ -17513,8 +17513,8 @@ function ensureShipSshKeypair(stateDir) {
17513
17513
  "-q"
17514
17514
  ], { stdio: ["ignore", "ignore", "pipe"] });
17515
17515
  } catch (e) {
17516
- const err = e;
17517
- throw new SshKeyError(`ssh-keygen failed: ${err.stderr?.toString().trim() || err.message}`);
17516
+ const err2 = e;
17517
+ throw new SshKeyError(`ssh-keygen failed: ${err2.stderr?.toString().trim() || err2.message}`);
17518
17518
  }
17519
17519
  chmodSync2(privateKeyPath, 384);
17520
17520
  }
@@ -17540,9 +17540,9 @@ var realExec = (cmd, args, opts) => {
17540
17540
  try {
17541
17541
  return execFileSync9(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: opts?.timeoutMs });
17542
17542
  } catch (e) {
17543
- const err = e;
17544
- const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
17545
- throw new Error(`${cmd} ${args.join(" ")}: ${why || err.message.trim()}`);
17543
+ const err2 = e;
17544
+ const why = [err2.stderr, err2.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
17545
+ throw new Error(`${cmd} ${args.join(" ")}: ${why || err2.message.trim()}`);
17546
17546
  }
17547
17547
  };
17548
17548
  var DOCKER_INFO_TIMEOUT_MS = 1e4;
@@ -17583,11 +17583,11 @@ function mountsDigest(mounts, image) {
17583
17583
  function planContainers(cfg) {
17584
17584
  const byWorkspace = /* @__PURE__ */ new Map();
17585
17585
  const image = passengerImageRef(cfg);
17586
- for (const route of cfg.routes) {
17587
- if (!route.enabled || !route.hostPassengers || !route.resolved?.workspaceId) continue;
17588
- const workspaceId = route.resolved.workspaceId;
17586
+ for (const route2 of cfg.routes) {
17587
+ if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
17588
+ const workspaceId = route2.resolved.workspaceId;
17589
17589
  const mounts = byWorkspace.get(workspaceId) ?? /* @__PURE__ */ new Map();
17590
- for (const target of passengerCheckoutTargets(route)) {
17590
+ for (const target of passengerCheckoutTargets(route2)) {
17591
17591
  mounts.set(target.name, { hostPath: target.dir, containerPath: `/workspace/${target.name}` });
17592
17592
  }
17593
17593
  byWorkspace.set(workspaceId, mounts);
@@ -17701,17 +17701,17 @@ function syncPassengerContainers(cfg, tablationApiBaseUrl, exec = realExec, sync
17701
17701
  }
17702
17702
  function passengerCheckoutSyncSpecs(cfg, workspaceId) {
17703
17703
  const specs = [];
17704
- for (const route of cfg.routes) {
17705
- if (!route.enabled || !route.hostPassengers || !route.resolved?.workspaceId) continue;
17706
- if (workspaceId && route.resolved.workspaceId !== workspaceId) continue;
17707
- const routeWorkspaceId = route.resolved.workspaceId;
17708
- const ordinaryByName = new Map(reposOf(route).map((t) => [t.name, t]));
17709
- for (const target of passengerCheckoutTargets(route)) {
17704
+ for (const route2 of cfg.routes) {
17705
+ if (!route2.enabled || !route2.hostPassengers || !route2.resolved?.workspaceId) continue;
17706
+ if (workspaceId && route2.resolved.workspaceId !== workspaceId) continue;
17707
+ const routeWorkspaceId = route2.resolved.workspaceId;
17708
+ const ordinaryByName = new Map(reposOf(route2).map((t) => [t.name, t]));
17709
+ for (const target of passengerCheckoutTargets(route2)) {
17710
17710
  const ordinary = ordinaryByName.get(target.name);
17711
- const override = route.repoOverrides[target.name];
17711
+ const override = route2.repoOverrides[target.name];
17712
17712
  const effective = resolveRepoConfig(
17713
17713
  ordinary ? loadRepoConfig(ordinary.dir) : null,
17714
- { branch: { ...route.branch, ...override?.branch } },
17714
+ { branch: { ...route2.branch, ...override?.branch } },
17715
17715
  ordinary?.dir ?? target.dir
17716
17716
  );
17717
17717
  specs.push({
@@ -18115,8 +18115,8 @@ function runSyncDaemonFromEnv(env = process.env) {
18115
18115
  const secret = requireEnv(env, "PASSENGER_SYNC_SECRET");
18116
18116
  const targets = JSON.parse(requireEnv(env, "PASSENGER_SYNC_TARGETS"));
18117
18117
  const server = createSyncDaemonServer(targets, secret);
18118
- server.on("error", (err) => {
18119
- console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${err instanceof Error ? err.message : String(err)}`);
18118
+ server.on("error", (err2) => {
18119
+ console.error(`Passenger sync daemon failed to bind 0.0.0.0:${port}: ${err2 instanceof Error ? err2.message : String(err2)}`);
18120
18120
  process.exit(1);
18121
18121
  });
18122
18122
  server.listen(port, "0.0.0.0", () => {
@@ -18233,22 +18233,22 @@ function codeOf(e) {
18233
18233
  const code = e?.code;
18234
18234
  return typeof code === "string" ? code : void 0;
18235
18235
  }
18236
- function isTransientError(err) {
18237
- if (!(err instanceof Error)) return false;
18238
- const msg = err.message;
18236
+ function isTransientError(err2) {
18237
+ if (!(err2 instanceof Error)) return false;
18238
+ const msg = err2.message;
18239
18239
  if (msg === "fetch failed" || msg.startsWith("fetch failed")) return true;
18240
- for (let e = err, depth = 0; e && depth < 4; e = e.cause, depth++) {
18240
+ for (let e = err2, depth = 0; e && depth < 4; e = e.cause, depth++) {
18241
18241
  const code = codeOf(e);
18242
18242
  if (code && NETWORK_CODES.includes(code)) return true;
18243
18243
  }
18244
18244
  if (NETWORK_CODES.some((c) => msg.includes(c))) return true;
18245
18245
  if (/\b(?:HTTP|status)\s*:?\s*(502|503|504)\b/i.test(msg) || /^(502|503|504)\b/.test(msg)) return true;
18246
- if (err instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
18246
+ if (err2 instanceof SyntaxError && /Unexpected token '?<|is not valid JSON|Unexpected end of JSON/.test(msg)) return true;
18247
18247
  return false;
18248
18248
  }
18249
- function describeCause(err) {
18250
- const first = (err instanceof Error ? err.message : String(err)).split("\n")[0];
18251
- const code = codeOf(err?.cause);
18249
+ function describeCause(err2) {
18250
+ const first = (err2 instanceof Error ? err2.message : String(err2)).split("\n")[0];
18251
+ const code = codeOf(err2?.cause);
18252
18252
  return code && !first.includes(code) ? `${first} (${code})` : first;
18253
18253
  }
18254
18254
 
@@ -18277,22 +18277,22 @@ async function runDaemonLoop(o) {
18277
18277
  o.onRecovered?.({ downMs: now() - downSince });
18278
18278
  transientRun = 0;
18279
18279
  }
18280
- } catch (err) {
18281
- if (isTransientError(err)) {
18280
+ } catch (err2) {
18281
+ if (isTransientError(err2)) {
18282
18282
  passes++;
18283
18283
  if (transientRun === 0) downSince = now();
18284
18284
  transientRun++;
18285
18285
  const wait = transientBackoffMs(transientRun);
18286
18286
  const downMs = now() - downSince;
18287
- o.onTransient?.({ err, consecutive: transientRun, backoffMs: wait, downMs });
18288
- if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err, consecutive: transientRun, downMs });
18287
+ o.onTransient?.({ err: err2, consecutive: transientRun, backoffMs: wait, downMs });
18288
+ if (transientRun === OUTAGE_ESCALATE_AFTER) o.onOutage?.({ err: err2, consecutive: transientRun, downMs });
18289
18289
  if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
18290
18290
  if (o.signal?.aborted) return "aborted";
18291
18291
  await sleep(wait);
18292
18292
  continue;
18293
18293
  }
18294
18294
  passes++;
18295
- o.onPassError?.(err);
18295
+ o.onPassError?.(err2);
18296
18296
  if (o.maxPasses !== void 0 && passes >= o.maxPasses) return "maxPasses";
18297
18297
  if (o.signal?.aborted) return "aborted";
18298
18298
  await sleep(backoff);
@@ -18411,7 +18411,7 @@ async function runOnePassUnguarded(o) {
18411
18411
  await hydrateApiKeys([o.route], o.getSessionStore);
18412
18412
  const emit = o.newEmitter();
18413
18413
  const decide = o.decide ?? decideCycle;
18414
- const spawn7 = o.spawnRoleAgent ?? runRoleAgent;
18414
+ const spawn8 = o.spawnRoleAgent ?? runRoleAgent;
18415
18415
  const breaker = o.spawnBreaker ?? new SpawnBreaker();
18416
18416
  const ctx = { route: o.route, ship: o.ship, state: o.state, emit, getSessionStore: o.getSessionStore };
18417
18417
  if (o.configFile) await reconcileRouteShipName({ route: o.route, ship: o.ship, configFile: o.configFile, emit });
@@ -18515,7 +18515,7 @@ async function runOnePassUnguarded(o) {
18515
18515
  const settled = new Promise((resolve6) => {
18516
18516
  ready = resolve6;
18517
18517
  });
18518
- const run2 = spawn7(role, decision, ctx, { ready }).then(() => {
18518
+ const run2 = spawn8(role, decision, ctx, { ready }).then(() => {
18519
18519
  breaker.recordSuccess(role);
18520
18520
  clearShipAttention(o.state, `role_parked:${role}`);
18521
18521
  }).catch(async (e) => {
@@ -18571,23 +18571,23 @@ async function publishShipAttention(o) {
18571
18571
  }
18572
18572
  }
18573
18573
  async function replayPendingWrites(ctx) {
18574
- const { route, ship, emit } = ctx;
18574
+ const { route: route2, ship, emit } = ctx;
18575
18575
  try {
18576
- const roles = [...new Set(listPendingHandoffs(ship.stateDir, routeSlug(route.route)).map((r) => r.role))];
18576
+ const roles = [...new Set(listPendingHandoffs(ship.stateDir, routeSlug(route2.route)).map((r) => r.role))];
18577
18577
  for (const role of roles) {
18578
- const identity = await resolveSeatKey(route, role, ctx.getSessionStore);
18579
- await replayRoutePending(route, ship, new Tracker(route, ship, { apiKey: identity.apiKey }), emit, role);
18578
+ const identity = await resolveSeatKey(route2, role, ctx.getSessionStore);
18579
+ await replayRoutePending(route2, ship, new Tracker(route2, ship, { apiKey: identity.apiKey }), emit, role);
18580
18580
  }
18581
18581
  await replayQueuedAgentRuns(
18582
18582
  ship.stateDir,
18583
- routeSlug(route.route),
18584
- async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
18583
+ routeSlug(route2.route),
18584
+ async (role) => (await resolveSeatKey(route2, role, ctx.getSessionStore)).apiKey,
18585
18585
  emit
18586
18586
  );
18587
18587
  await replayQueuedTicketCounters(
18588
18588
  ship.stateDir,
18589
- routeSlug(route.route),
18590
- async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
18589
+ routeSlug(route2.route),
18590
+ async (role) => (await resolveSeatKey(route2, role, ctx.getSessionStore)).apiKey,
18591
18591
  emit
18592
18592
  );
18593
18593
  } catch (e) {
@@ -18595,27 +18595,27 @@ async function replayPendingWrites(ctx) {
18595
18595
  }
18596
18596
  }
18597
18597
  async function reconcileForRoute(ctx, o) {
18598
- const { route, ship, emit } = ctx;
18598
+ const { route: route2, ship, emit } = ctx;
18599
18599
  await replayPendingWrites(ctx);
18600
18600
  await reconcileRuns({
18601
18601
  stateDir: ship.stateDir,
18602
- routeLabel: routeSlug(route.route),
18602
+ routeLabel: routeSlug(route2.route),
18603
18603
  emit,
18604
18604
  state: o.state,
18605
18605
  maxConcurrentAgents: o.maxConcurrentAgents,
18606
18606
  roleConcurrency: o.roleConcurrency,
18607
18607
  inFlight: o.inFlight,
18608
- agentLogKey: async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
18608
+ agentLogKey: async (role) => (await resolveSeatKey(route2, role, ctx.getSessionStore)).apiKey,
18609
18609
  afterRun: async (sc, result) => {
18610
- const identity = await resolveSeatKey(route, sc.role, ctx.getSessionStore);
18611
- const tracker = new Tracker(route, ship, { apiKey: identity.apiKey });
18612
- const memberId = route.resolved?.seats[sc.role];
18610
+ const identity = await resolveSeatKey(route2, sc.role, ctx.getSessionStore);
18611
+ const tracker = new Tracker(route2, ship, { apiKey: identity.apiKey });
18612
+ const memberId = route2.resolved?.seats[sc.role];
18613
18613
  try {
18614
18614
  if (!sc.ticketRecordId) return;
18615
18615
  if (result.interrupted) {
18616
18616
  if (!memberId) return;
18617
18617
  const t = await tracker.ticket(sc.ticketRecordId);
18618
- const target = repoTargetFor(route, t.repo_id);
18618
+ const target = repoTargetFor(route2, t.repo_id);
18619
18619
  const num = String(t[tracker.contract.columns.key] ?? "").replace(/^\D+/, "");
18620
18620
  const wt = target && num ? worktreeForNumber(target.dir, num) : null;
18621
18621
  let dirty = null;
@@ -18629,13 +18629,13 @@ async function reconcileForRoute(ctx, o) {
18629
18629
  reason: result.interrupted === "daemon-exit" ? "the crew daemon exited" : "it exited without a result"
18630
18630
  }), memberId, "interrupted");
18631
18631
  } else {
18632
- await guardRunHandoff(route, ship, tracker, sc.role, sc.ticketRecordId, emit, sc.spawnedAt);
18632
+ await guardRunHandoff(route2, ship, tracker, sc.role, sc.ticketRecordId, emit, sc.spawnedAt);
18633
18633
  }
18634
18634
  } finally {
18635
18635
  await settleRunEnd({
18636
18636
  tracker,
18637
18637
  shipName: ship.name,
18638
- route,
18638
+ route: route2,
18639
18639
  role: sc.role,
18640
18640
  memberId,
18641
18641
  ownTicketRecordId: sc.ticketRecordId,
@@ -18656,30 +18656,30 @@ async function runRoleAgent(role, decision, ctx, hooks) {
18656
18656
  }
18657
18657
  }
18658
18658
  async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
18659
- const { route, ship, emit } = ctx;
18659
+ const { route: route2, ship, emit } = ctx;
18660
18660
  const runId = randomBytes2(3).toString("hex");
18661
- const identity = await resolveSeatKey(route, role, ctx.getSessionStore);
18661
+ const identity = await resolveSeatKey(route2, role, ctx.getSessionStore);
18662
18662
  if (identity.kind === "ship" && SEAT_KEY_ROLES.includes(role)) {
18663
18663
  emit.warn(`${role} writes as the ship, not its own agent: ${identity.reason}`, { step: "agent", role });
18664
18664
  } else if (identity.kind === "agent") {
18665
18665
  emit.emit(`${role} writes as its own agent principal`, { step: "agent", role });
18666
18666
  }
18667
- const tracker = new Tracker(route, ship, { apiKey: identity.apiKey });
18667
+ const tracker = new Tracker(route2, ship, { apiKey: identity.apiKey });
18668
18668
  let ticketHint = decision.actionable.top?.issue_id;
18669
- let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
18669
+ let ticketRepo = repoTargetFor(route2, decision.actionable.top?.repo_id)?.name;
18670
18670
  let workingId = decision.actionable.top?.id ?? null;
18671
18671
  if (role === "dev" || role === "design" || role === "qa") {
18672
- const seat = route.resolved?.seats[role];
18672
+ const seat = route2.resolved?.seats[role];
18673
18673
  if (seat) {
18674
- const candidates = rankedCandidates(role, decision.selectionInput);
18674
+ const candidates2 = rankedCandidates(role, decision.selectionInput);
18675
18675
  const contract = decision.selectionInput.contract ?? DEFAULT_CONTRACT;
18676
18676
  const result = await resolveTopCandidate(
18677
18677
  tracker,
18678
- candidates,
18678
+ candidates2,
18679
18679
  seat,
18680
18680
  claimStatuses(role, contract).approved,
18681
18681
  claimStatuses(role, contract).building,
18682
- (t) => dirForRepo(route, t.repo_id) !== null,
18682
+ (t) => dirForRepo(route2, t.repo_id) !== null,
18683
18683
  withHoldCheck(await tracker.claimAffinity(), decision.selectionInput),
18684
18684
  (t) => {
18685
18685
  const got = ctx.state.acquireTicket(t.id);
@@ -18710,7 +18710,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
18710
18710
  decision.selectionInput.ships?.rows ?? [],
18711
18711
  ship.name,
18712
18712
  (t) => {
18713
- const d = dirForRepo(route, t.repo_id);
18713
+ const d = dirForRepo(route2, t.repo_id);
18714
18714
  return d ? originBranchSha(d, t, resolve2) : null;
18715
18715
  }
18716
18716
  );
@@ -18721,16 +18721,16 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
18721
18721
  if (result.claimed) {
18722
18722
  const idx = decision.tickets.findIndex((t) => t.id === result.ticket.id);
18723
18723
  if (idx >= 0) decision.tickets[idx] = result.ticket;
18724
- writeDigest({ route, ship, state: ctx.state, emit }, decision, role, ship.stateDir);
18724
+ writeDigest({ route: route2, ship, state: ctx.state, emit }, decision, role, ship.stateDir);
18725
18725
  }
18726
- writeDigest({ route, ship, state: ctx.state, emit }, decision, role, ship.stateDir, runId, {
18726
+ writeDigest({ route: route2, ship, state: ctx.state, emit }, decision, role, ship.stateDir, runId, {
18727
18727
  ticket: result.ticket,
18728
18728
  inFlight: (id) => ctx.state.ticketInFlight(id)
18729
18729
  });
18730
18730
  hooks?.ready(true);
18731
18731
  ticketHint = result.ticket.issue_id;
18732
18732
  workingId = result.ticket.id;
18733
- const target = repoTargetFor(route, result.ticket.repo_id);
18733
+ const target = repoTargetFor(route2, result.ticket.repo_id);
18734
18734
  ticketRepo = target?.name;
18735
18735
  try {
18736
18736
  if (target && ensureRepoCheckout(target.dir, target.remote)) {
@@ -18746,25 +18746,25 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
18746
18746
  }
18747
18747
  const plan = planAgentRun({
18748
18748
  role,
18749
- route,
18749
+ route: route2,
18750
18750
  ship,
18751
18751
  stateDir: ship.stateDir,
18752
- roster: rosterFor(decision, route, role),
18753
- environment: await buildEnvironment(route, ship, ticketHint),
18752
+ roster: rosterFor(decision, route2, role),
18753
+ environment: await buildEnvironment(route2, ship, ticketHint),
18754
18754
  apiKey: identity.apiKey,
18755
18755
  cycle: emit.cycle,
18756
18756
  runId,
18757
18757
  ticket: ticketHint,
18758
18758
  repo: ticketRepo,
18759
18759
  ticketRecordId: workingId,
18760
- ...personaPromptOptions(await fetchPersonaPrompt(route, role, { userAgent: ship.userAgent })),
18761
- skillOverrides: await fetchSkillOverrides(route, { userAgent: ship.userAgent }),
18762
- agentPersona: await fetchSeatPersona(route, role, { userAgent: ship.userAgent }),
18763
- resolvedAgentId: await resolveAgentId(route, role, { userAgent: ship.userAgent })
18760
+ ...personaPromptOptions(await fetchPersonaPrompt(route2, role, { userAgent: ship.userAgent })),
18761
+ skillOverrides: await fetchSkillOverrides(route2, { userAgent: ship.userAgent }),
18762
+ agentPersona: await fetchSeatPersona(route2, role, { userAgent: ship.userAgent }),
18763
+ resolvedAgentId: await resolveAgentId(route2, role, { userAgent: ship.userAgent })
18764
18764
  });
18765
18765
  emit.enter("agent", role);
18766
- emit.emit(`starting agent run for ${route.route}`);
18767
- const memberId = route.resolved?.seats[role];
18766
+ emit.emit(`starting agent run for ${route2.route}`);
18767
+ const memberId = route2.resolved?.seats[role];
18768
18768
  if (memberId) {
18769
18769
  try {
18770
18770
  await tracker.setCrewStatus(memberId, "working", workingId);
@@ -18773,7 +18773,7 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
18773
18773
  }
18774
18774
  }
18775
18775
  try {
18776
- await tracker.beatEngaged(ship.name, route.route, workingId);
18776
+ await tracker.beatEngaged(ship.name, route2.route, workingId);
18777
18777
  } catch (e) {
18778
18778
  emit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
18779
18779
  }
@@ -18792,12 +18792,12 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
18792
18792
  }
18793
18793
  }
18794
18794
  await spawnAgent(plan, emit, hintMoved);
18795
- await guardRunHandoff(route, ship, tracker, role, workingId, emit, runStart);
18795
+ await guardRunHandoff(route2, ship, tracker, role, workingId, emit, runStart);
18796
18796
  } finally {
18797
18797
  await settleRunEnd({
18798
18798
  tracker,
18799
18799
  shipName: ship.name,
18800
- route,
18800
+ route: route2,
18801
18801
  role,
18802
18802
  memberId,
18803
18803
  ownTicketRecordId: workingId,
@@ -18806,31 +18806,441 @@ async function runRoleAgentLocked(role, decision, ctx, hooks, held) {
18806
18806
  });
18807
18807
  }
18808
18808
  }
18809
- async function buildEnvironment(route, ship, ticket) {
18810
- const tracker = new Tracker(route, ship);
18809
+ async function buildEnvironment(route2, ship, ticket) {
18810
+ const tracker = new Tracker(route2, ship);
18811
18811
  const contract = tracker.contract;
18812
- const repos = reposOf(route).map((t) => {
18813
- const id = repoIdForName(route, t.name);
18814
- const o = route.repoOverrides[t.name];
18812
+ const repos = reposOf(route2).map((t) => {
18813
+ const id = repoIdForName(route2, t.name);
18814
+ const o = route2.repoOverrides[t.name];
18815
18815
  return {
18816
18816
  name: t.name,
18817
18817
  dir: t.dir,
18818
18818
  id,
18819
18819
  config: resolveRepoConfig(loadRepoConfig(t.dir), {
18820
- hooks: { ...route.hooks, ...o?.hooks },
18821
- labels: { ...route.labels, ...o?.labels },
18822
- branch: { ...route.branch, ...o?.branch },
18820
+ hooks: { ...route2.hooks, ...o?.hooks },
18821
+ labels: { ...route2.labels, ...o?.labels },
18822
+ branch: { ...route2.branch, ...o?.branch },
18823
18823
  worktrees: { nameOverride: o?.worktreeName }
18824
18824
  }, t.dir)
18825
18825
  };
18826
18826
  });
18827
18827
  const authorship = await tracker.authorshipProbe().catch(() => null);
18828
18828
  const attachments = await tracker.attachmentsProbe().catch(() => null);
18829
- return renderEnvironment({ route, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name });
18829
+ return renderEnvironment({ route: route2, userAgent: ship.userAgent, repos, contract, sourceTicket: ticket, authorship, attachments, shipName: ship.name });
18830
+ }
18831
+
18832
+ // src/control-api.ts
18833
+ import { createHash as createHash8 } from "crypto";
18834
+ import {
18835
+ chmodSync as chmodSync3,
18836
+ closeSync as closeSync5,
18837
+ existsSync as existsSync38,
18838
+ fstatSync,
18839
+ openSync as openSync5,
18840
+ readSync as readSync3,
18841
+ readdirSync as readdirSync17,
18842
+ rmSync as rmSync17,
18843
+ statSync as statSync12,
18844
+ watch
18845
+ } from "fs";
18846
+ import { createConnection } from "net";
18847
+ import { createServer } from "http";
18848
+ import { basename as basename12, dirname as dirname21, join as join42 } from "path";
18849
+ import { execFile, spawn as spawn7 } from "child_process";
18850
+ var PAUSABLE = ["dev", "design", "qa"];
18851
+ var CONTROL_SOCKET_NAME = "control.sock";
18852
+ var controlSocketFor = (stateDir) => join42(stateDir, CONTROL_SOCKET_NAME);
18853
+ var MAX_SOCKET_PATH = 100;
18854
+ var MAX_TAIL_BYTES = 1024 * 1024;
18855
+ var MAX_WAIT_SECONDS = 30;
18856
+ var DEFAULT_TAIL_LINES = 100;
18857
+ var MAX_BODY_BYTES = 4096;
18858
+ var ControlAuth = class {
18859
+ cache = /* @__PURE__ */ new Map();
18860
+ o;
18861
+ constructor(o) {
18862
+ this.o = o;
18863
+ }
18864
+ async verify(token) {
18865
+ if (!token) return "unauthorized";
18866
+ for (const k of this.o.shipKeys()) if (k && secretsMatch(token, k)) return "ok";
18867
+ const now = (this.o.now ?? Date.now)();
18868
+ const id = createHash8("sha256").update(token).digest("hex");
18869
+ const hit = this.cache.get(id);
18870
+ if (hit && now - hit.at < (this.o.cacheMs ?? 6e4)) return hit.verdict;
18871
+ const verdict = await this.ask(token);
18872
+ if (verdict !== "unavailable") {
18873
+ this.cache.set(id, { verdict, at: now });
18874
+ if (this.cache.size > 256) {
18875
+ for (const [k, v] of this.cache) if (now - v.at >= (this.o.cacheMs ?? 6e4)) this.cache.delete(k);
18876
+ }
18877
+ }
18878
+ return verdict;
18879
+ }
18880
+ async ask(token) {
18881
+ const doFetch = this.o.fetch ?? fetch;
18882
+ const base = this.o.baseUrl.replace(/\/+$/, "");
18883
+ let forbidden = false;
18884
+ for (const ws of this.o.workspaceIds) {
18885
+ let res;
18886
+ try {
18887
+ res = await doFetch(`${base}/api/auth/me?workspaceId=${encodeURIComponent(ws)}`, {
18888
+ headers: { Authorization: `Bearer ${token}`, "User-Agent": this.o.userAgent ?? "Mozilla/5.0 CrewShipControl/1.0" },
18889
+ signal: AbortSignal.timeout(this.o.timeoutMs ?? 5e3)
18890
+ });
18891
+ } catch {
18892
+ return "unavailable";
18893
+ }
18894
+ if (res.status === 401) return "unauthorized";
18895
+ if (res.status === 403) {
18896
+ forbidden = true;
18897
+ continue;
18898
+ }
18899
+ if (res.status !== 200) return "unavailable";
18900
+ if (!(res.headers.get("content-type") ?? "").includes("application/json")) return "unavailable";
18901
+ return "ok";
18902
+ }
18903
+ return forbidden ? "forbidden" : "unauthorized";
18904
+ }
18905
+ };
18906
+ function encodeCursor(c) {
18907
+ return Buffer.from(JSON.stringify([c.dev, c.ino, c.off])).toString("base64url");
18908
+ }
18909
+ function decodeCursor(s) {
18910
+ try {
18911
+ const a = JSON.parse(Buffer.from(s, "base64url").toString("utf8"));
18912
+ if (Array.isArray(a) && a.length === 3 && a.every((n) => Number.isSafeInteger(n) && n >= 0)) {
18913
+ return { dev: a[0], ino: a[1], off: a[2] };
18914
+ }
18915
+ } catch {
18916
+ }
18917
+ return null;
18918
+ }
18919
+ function candidates(live, rotated) {
18920
+ const out = [live];
18921
+ if (!rotated) return out;
18922
+ try {
18923
+ for (const n of readdirSync17(dirname21(live))) if (ROTATED_EVENTS_RE.test(n)) out.push(join42(dirname21(live), n));
18924
+ } catch {
18925
+ }
18926
+ return out;
18927
+ }
18928
+ function readRange(path, from, max) {
18929
+ const fd = openSync5(path, "r");
18930
+ try {
18931
+ const size = fstatSync(fd).size;
18932
+ const len = Math.max(0, Math.min(size - from, max));
18933
+ const buf = Buffer.alloc(len);
18934
+ let got = 0;
18935
+ while (got < len) {
18936
+ const n = readSync3(fd, buf, got, len - got, from + got);
18937
+ if (n <= 0) break;
18938
+ got += n;
18939
+ }
18940
+ return { text: buf.subarray(0, got).toString("utf8"), size };
18941
+ } finally {
18942
+ closeSync5(fd);
18943
+ }
18944
+ }
18945
+ function lastLines2(path, n) {
18946
+ const st = statSync12(path);
18947
+ const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
18948
+ if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
18949
+ let start = st.size;
18950
+ let text = "";
18951
+ while (start > 0 && text.split("\n").length <= n + 1 && st.size - start < MAX_TAIL_BYTES) {
18952
+ const step = Math.min(64 * 1024, start);
18953
+ start -= step;
18954
+ text = readRange(path, start, step).text + text;
18955
+ }
18956
+ let lines = text.split("\n");
18957
+ if (start > 0) lines = lines.slice(1);
18958
+ let end = st.size;
18959
+ if (!text.endsWith("\n")) {
18960
+ const frag = lines.pop() ?? "";
18961
+ end -= Buffer.byteLength(frag);
18962
+ } else lines.pop();
18963
+ return { lines: lines.slice(-n).filter((l) => l.length), cursor: cursorAt(end), reset: false };
18964
+ }
18965
+ function readTail(live, opts) {
18966
+ const max = opts.maxBytes ?? MAX_TAIL_BYTES;
18967
+ if (!existsSync38(live)) {
18968
+ return { lines: [], cursor: opts.after ?? encodeCursor({ dev: 0, ino: 0, off: 0 }), reset: false };
18969
+ }
18970
+ const cur = opts.after ? decodeCursor(opts.after) : null;
18971
+ if (!opts.after) return lastLines2(live, opts.tail ?? DEFAULT_TAIL_LINES);
18972
+ let reset = false;
18973
+ let path = live;
18974
+ let off = 0;
18975
+ if (!cur) {
18976
+ reset = true;
18977
+ } else {
18978
+ const found = candidates(live, opts.rotated ?? false).find((p) => {
18979
+ try {
18980
+ const s = statSync12(p);
18981
+ return s.ino === cur.ino && s.dev === cur.dev;
18982
+ } catch {
18983
+ return false;
18984
+ }
18985
+ });
18986
+ if (found) {
18987
+ path = found;
18988
+ off = cur.off;
18989
+ } else reset = true;
18990
+ }
18991
+ let size = statSync12(path).size;
18992
+ if (off > size) {
18993
+ off = 0;
18994
+ reset = true;
18995
+ }
18996
+ if (off >= size && path !== live) {
18997
+ path = live;
18998
+ off = 0;
18999
+ size = statSync12(path).size;
19000
+ }
19001
+ const { text } = readRange(path, off, max);
19002
+ const cut = text.lastIndexOf("\n");
19003
+ const whole = cut < 0 ? "" : text.slice(0, cut + 1);
19004
+ const st = statSync12(path);
19005
+ return {
19006
+ lines: whole.split("\n").filter((l) => l.length),
19007
+ cursor: encodeCursor({ dev: st.dev, ino: st.ino, off: off + Buffer.byteLength(whole) }),
19008
+ reset
19009
+ };
19010
+ }
19011
+ function waitForChange(dir, ms3, signal) {
19012
+ return new Promise((resolve6) => {
19013
+ let w;
19014
+ const done = () => {
19015
+ clearTimeout(t);
19016
+ clearInterval(poll);
19017
+ signal.removeEventListener("abort", done);
19018
+ try {
19019
+ w?.close();
19020
+ } catch {
19021
+ }
19022
+ resolve6();
19023
+ };
19024
+ const t = setTimeout(done, ms3);
19025
+ const poll = setInterval(done, 500);
19026
+ signal.addEventListener("abort", done);
19027
+ try {
19028
+ w = watch(dir, done);
19029
+ w.on("error", () => {
19030
+ });
19031
+ } catch {
19032
+ }
19033
+ });
19034
+ }
19035
+ var reply = (status2, body) => ({ status: status2, body });
19036
+ var err = (status2, message) => reply(status2, { error: message });
19037
+ function pauseDoc(state) {
19038
+ return {
19039
+ crew: state.isPaused(),
19040
+ release: state.isReleasePaused(),
19041
+ roles: Object.fromEntries(PAUSABLE.map((r) => [r, state.isRolePaused(r)]))
19042
+ };
19043
+ }
19044
+ function describeStream(r) {
19045
+ return {
19046
+ run: basename12(r.streamPath).replace(/\.jsonl$/, ""),
19047
+ pid: r.pid,
19048
+ ticket: r.ticket,
19049
+ role: r.role,
19050
+ model: r.model,
19051
+ startedAt: r.spawnedAt,
19052
+ // No run-time limit is enforced today; the field is here so a reader needs no change when one is.
19053
+ deadline: null
19054
+ };
19055
+ }
19056
+ var RUN_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
19057
+ async function readJsonBody(req) {
19058
+ const chunks = [];
19059
+ let n = 0;
19060
+ for await (const c of req) {
19061
+ n += c.length;
19062
+ if (n > MAX_BODY_BYTES) return null;
19063
+ chunks.push(c);
19064
+ }
19065
+ if (!n) return {};
19066
+ try {
19067
+ const v = JSON.parse(Buffer.concat(chunks).toString("utf8"));
19068
+ return v && typeof v === "object" && !Array.isArray(v) ? v : null;
19069
+ } catch {
19070
+ return null;
19071
+ }
19072
+ }
19073
+ async function tailReply(path, q, rotated, abort) {
19074
+ const after = q.get("after") ?? void 0;
19075
+ const tail3 = q.has("tail") ? Number(q.get("tail")) : void 0;
19076
+ if (tail3 !== void 0 && !(Number.isInteger(tail3) && tail3 >= 0 && tail3 <= 1e3)) return err(400, "tail must be an integer from 0 to 1000");
19077
+ const wait = q.has("wait") ? Number(q.get("wait")) : 0;
19078
+ if (!(Number.isFinite(wait) && wait >= 0)) return err(400, "wait must be a number of seconds");
19079
+ if (after !== void 0 && !decodeCursor(after)) return err(400, "after is not a cursor this API issued");
19080
+ const deadline = Date.now() + Math.min(wait, MAX_WAIT_SECONDS) * 1e3;
19081
+ let r = readTail(path, { after, tail: tail3, rotated });
19082
+ while (after && !r.lines.length && !r.reset && Date.now() < deadline && !abort.aborted) {
19083
+ await waitForChange(dirname21(path), deadline - Date.now(), abort);
19084
+ r = readTail(path, { after: r.cursor, rotated });
19085
+ }
19086
+ return reply(200, r);
19087
+ }
19088
+ async function route(deps, method, url, req, abort) {
19089
+ const p = url.pathname.replace(/\/+$/, "") || "/";
19090
+ const get2 = method === "GET";
19091
+ const post3 = method === "POST";
19092
+ if (p === "/status" && get2) {
19093
+ const doc = await deps.status();
19094
+ if (!doc) return err(502, "could not produce the status document");
19095
+ return reply(200, {
19096
+ ...doc,
19097
+ daemon: {
19098
+ alive: true,
19099
+ pid: process.pid,
19100
+ startedAt: deps.startedAt ?? readDaemonStarted(deps.stateDir),
19101
+ uptimeSeconds: Math.round(process.uptime())
19102
+ },
19103
+ pause: pauseDoc(deps.state)
19104
+ });
19105
+ }
19106
+ if (p === "/events" && get2) return tailReply(eventFileFor(deps.stateDir), url.searchParams, true, abort);
19107
+ if (p === "/streams" && get2) return reply(200, { streams: liveRuns(deps.stateDir).map(describeStream) });
19108
+ if (p.startsWith("/streams/") && get2) {
19109
+ const run2 = decodeURIComponent(p.slice("/streams/".length));
19110
+ if (!RUN_NAME_RE.test(run2) || run2.includes("..") || /\.(pid|meta\.json|events\.jsonl)$/.test(run2)) return err(400, "not a run name");
19111
+ const file = join42(deps.stateDir, "streams", run2.endsWith(".jsonl") ? run2 : `${run2}.jsonl`);
19112
+ if (!existsSync38(file)) return err(404, "no such run");
19113
+ return tailReply(file, url.searchParams, false, abort);
19114
+ }
19115
+ if ((p === "/pause" || p === "/resume") && post3) {
19116
+ const body = await readJsonBody(req);
19117
+ if (!body) return err(400, "body must be a small JSON object");
19118
+ const role = body.role;
19119
+ if (role !== void 0 && !PAUSABLE.includes(role)) return err(400, `role must be one of ${PAUSABLE.join(", ")}`);
19120
+ if (p === "/pause") deps.state.pause(role);
19121
+ else deps.state.resume(role);
19122
+ return reply(200, { ok: true, pause: pauseDoc(deps.state) });
19123
+ }
19124
+ if ((p === "/release/pause" || p === "/release/resume") && post3) {
19125
+ deps.state.setReleasePaused(p === "/release/pause");
19126
+ return reply(200, { ok: true, pause: pauseDoc(deps.state) });
19127
+ }
19128
+ const sched = /^\/scheduler\/(start|stop|restart)$/.exec(p);
19129
+ if (sched && post3) {
19130
+ const r = deps.scheduler(sched[1]);
19131
+ return r.accepted ? reply(202, { accepted: true, detail: r.detail }) : err(409, r.detail);
19132
+ }
19133
+ if (p === "/doctor" && get2) {
19134
+ const doc = await deps.doctor();
19135
+ return doc ? reply(200, doc) : err(502, "could not produce the doctor report");
19136
+ }
19137
+ const known = ["/status", "/events", "/streams", "/pause", "/resume", "/release/pause", "/release/resume", "/doctor"];
19138
+ if (known.includes(p) || p.startsWith("/streams/") || sched) return err(405, "method not allowed");
19139
+ return err(404, "not found");
19140
+ }
19141
+ function send(res, r) {
19142
+ const body = JSON.stringify(r.body);
19143
+ res.writeHead(r.status, { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(body), "Cache-Control": "no-store" });
19144
+ res.end(body);
19145
+ }
19146
+ async function socketIsStale(path) {
19147
+ if (!existsSync38(path)) return false;
19148
+ return new Promise((resolve6) => {
19149
+ const c = createConnection(path);
19150
+ c.once("connect", () => {
19151
+ c.destroy();
19152
+ resolve6(false);
19153
+ });
19154
+ c.once("error", () => resolve6(true));
19155
+ });
19156
+ }
19157
+ async function startControlServer(deps) {
19158
+ const path = controlSocketFor(deps.stateDir);
19159
+ const log = deps.log ?? (() => {
19160
+ });
19161
+ if (Buffer.byteLength(path) > MAX_SOCKET_PATH) {
19162
+ log(`control API not started: socket path is too long for a unix socket (${path})`);
19163
+ return null;
19164
+ }
19165
+ if (await socketIsStale(path)) rmSync17(path, { force: true });
19166
+ else if (existsSync38(path)) {
19167
+ log(`control API not started: ${path} is in use by another process`);
19168
+ return null;
19169
+ }
19170
+ const server = createServer((req, res) => {
19171
+ const abort = new AbortController();
19172
+ res.on("close", () => abort.abort());
19173
+ void (async () => {
19174
+ try {
19175
+ const m = /^Bearer (.+)$/.exec(req.headers.authorization ?? "");
19176
+ const verdict = await deps.auth.verify(m?.[1]?.trim() ?? "");
19177
+ if (verdict === "unauthorized") return send(res, err(401, "invalid or revoked key"));
19178
+ if (verdict === "forbidden") return send(res, err(403, "this key does not belong to a workspace this ship serves"));
19179
+ if (verdict === "unavailable") return send(res, err(503, "could not verify the key right now"));
19180
+ send(res, await route(deps, req.method ?? "GET", new URL(req.url ?? "/", "http://ship"), req, abort.signal));
19181
+ } catch (e) {
19182
+ log(`control API request failed: ${e.message}`);
19183
+ if (!res.headersSent) send(res, err(500, "internal error"));
19184
+ else res.end();
19185
+ }
19186
+ })();
19187
+ });
19188
+ return new Promise((resolve6) => {
19189
+ server.once("error", (e) => {
19190
+ log(`control API not started: ${e.message}`);
19191
+ resolve6(null);
19192
+ });
19193
+ server.listen(path, () => {
19194
+ try {
19195
+ chmodSync3(path, 384);
19196
+ } catch {
19197
+ }
19198
+ server.on("error", (e) => log(`control API error: ${e.message}`));
19199
+ resolve6({
19200
+ server,
19201
+ path,
19202
+ close: () => new Promise((done) => {
19203
+ server.close(() => {
19204
+ rmSync17(path, { force: true });
19205
+ done();
19206
+ });
19207
+ })
19208
+ });
19209
+ });
19210
+ });
19211
+ }
19212
+ var shq2 = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
19213
+ function runDetached(commands, spawnFn = spawn7) {
19214
+ const script = `sleep 1; ${commands.map((c) => c.map(shq2).join(" ")).join("; ")}`;
19215
+ const child = spawnFn("/bin/sh", ["-c", script], { detached: true, stdio: "ignore" });
19216
+ child.on("error", () => {
19217
+ });
19218
+ child.unref();
19219
+ }
19220
+ function schedulerHandler(control, run2 = runDetached) {
19221
+ return (action) => {
19222
+ const { startCommand: start, stopCommand: stop } = control;
19223
+ if (!start || !stop) return { accepted: false, detail: "this host has no service manager for the daemon (cron or none)" };
19224
+ const commands = action === "start" ? [start] : action === "stop" ? [stop] : [stop, start];
19225
+ run2(commands);
19226
+ return { accepted: true, detail: `${action} requested` };
19227
+ };
19228
+ }
19229
+ function crewJson(args, timeoutMs = 6e4) {
19230
+ return new Promise((resolve6) => {
19231
+ execFile(process.execPath, [process.argv[1] ?? "", ...args], { timeout: timeoutMs, maxBuffer: 16 * 1024 * 1024 }, (_e, stdout) => {
19232
+ try {
19233
+ const v = JSON.parse(String(stdout));
19234
+ resolve6(v && typeof v === "object" && !Array.isArray(v) ? v : null);
19235
+ } catch {
19236
+ resolve6(null);
19237
+ }
19238
+ });
19239
+ });
18830
19240
  }
18831
19241
 
18832
19242
  // src/cli.ts
18833
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname21(process.execPath) : resolve5(dirname21(fileURLToPath(import.meta.url)), "..");
19243
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname22(process.execPath) : resolve5(dirname22(fileURLToPath(import.meta.url)), "..");
18834
19244
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
18835
19245
  var CYCLE_SECONDS2 = 120;
18836
19246
  var STALE_CYCLES = 3;
@@ -19155,9 +19565,9 @@ if (command === "passenger-sync-daemon") {
19155
19565
  runSyncDaemonFromEnv();
19156
19566
  } else {
19157
19567
  let requireArmed = function(what) {
19158
- if (dryRun || route.enabled) return;
19568
+ if (dryRun || route2.enabled) return;
19159
19569
  process.stderr.write(
19160
- `crew: route "${route.route}" is not enabled in ${cfg.configFile} \u2014 refusing to ${what}.
19570
+ `crew: route "${route2.route}" is not enabled in ${cfg.configFile} \u2014 refusing to ${what}.
19161
19571
  Add --dry-run to see what it would do.
19162
19572
  `
19163
19573
  );
@@ -19268,7 +19678,7 @@ if (command === "passenger-sync-daemon") {
19268
19678
  } else {
19269
19679
  const toleratesNoRoutes = command === "credential" || command === "forge" || command === "repos" || command === "ship" || command === "disconnect" && flag("reset") || command === "connect" && flag("sign-out");
19270
19680
  const configless = command === "connect" && flag("sign-out") || command === "disconnect" && flag("reset");
19271
- if (configless && !existsSync38(findConfigFile(CREW_HOME))) {
19681
+ if (configless && !existsSync39(findConfigFile(CREW_HOME))) {
19272
19682
  cfg = loadConfig(CREW_HOME, firstRunConfigPath(), { text: "ship: {}\n", allowNoRoutes: true });
19273
19683
  } else {
19274
19684
  cfg = loadConfig(CREW_HOME, void 0, toleratesNoRoutes ? { allowNoRoutes: true } : {});
@@ -19318,9 +19728,9 @@ if (command === "passenger-sync-daemon") {
19318
19728
  const worktreeArgs = command === "worktree" ? splitWorktreeArgs(positional, cfg.routes.map((r) => r.route)) : void 0;
19319
19729
  const named = worktreeArgs ? worktreeArgs.route : command === "daemon" && DAEMON_SUBCOMMANDS.has(positional[1] ?? "") ? positional[2] : positional[1];
19320
19730
  const fleetWide = command === "inbox" || command === "connect" || command === "disconnect" || command === "agents" || command === "skills" || command === "logbook" || command === "repos" || command === "ship" || command === "projects" || command === "periscope" || command === "whoami" || releaseFleetWide || command === "pause" || command === "resume" || command === "reload" || FLEET_CAPABLE.has(command) && !named && cfg.routes.length > 1 || worktreeArgs !== void 0 && !named && cfg.routes.length > 1;
19321
- let route;
19731
+ let route2;
19322
19732
  try {
19323
- route = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
19733
+ route2 = fleetWide ? cfg.routes[0] : findRoute(cfg, named);
19324
19734
  } catch (e) {
19325
19735
  if (e instanceof ConfigError) {
19326
19736
  process.stderr.write(`crew: ${e.message}
@@ -19335,7 +19745,7 @@ if (command === "passenger-sync-daemon") {
19335
19745
  const force = flag("force");
19336
19746
  const EXCLUSIVE = { passengers: "passengers" };
19337
19747
  const emit = new Emitter({
19338
- route: route?.route ?? "",
19748
+ route: route2?.route ?? "",
19339
19749
  eventFile: dryRun ? void 0 : eventFileFor(cfg.ship.stateDir),
19340
19750
  rotateBytes: cfg.ship.eventsRotateBytes,
19341
19751
  logFile: dryRun ? void 0 : cfg.ship.logFile,
@@ -19434,18 +19844,18 @@ if (command === "passenger-sync-daemon") {
19434
19844
  if (releaseRefused()) return;
19435
19845
  precheckTickets.clear();
19436
19846
  const only = value("repo");
19437
- const targets = reposOf(route);
19847
+ const targets = reposOf(route2);
19438
19848
  const chosen = only ? targets.filter((t) => t.name === only) : targets;
19439
19849
  if (only && chosen.length === 0) {
19440
19850
  process.stderr.write(
19441
- `crew: no repo named "${only}" on route "${route.route}" (have: ${targets.map((t) => t.name).join(", ")})
19851
+ `crew: no repo named "${only}" on route "${route2.route}" (have: ${targets.map((t) => t.name).join(", ")})
19442
19852
  `
19443
19853
  );
19444
19854
  process.exit(2);
19445
19855
  }
19446
19856
  const summaries = [];
19447
19857
  if (!dryRun) state.releaseHeartbeat().touch();
19448
- for (const t of chosen) summaries.push({ ...await releasePhase(route, t, opts), route: route.route });
19858
+ for (const t of chosen) summaries.push({ ...await releasePhase(route2, t, opts), route: route2.route });
19449
19859
  emitReleaseSummary(emit, summaries);
19450
19860
  if (!dryRun) state.releaseHeartbeat().touch();
19451
19861
  try {
@@ -19533,8 +19943,8 @@ if (command === "passenger-sync-daemon") {
19533
19943
  }
19534
19944
  if (host === "linux" && hasSystemd) {
19535
19945
  const label = labelFor(CREW_HOME, "run");
19536
- const staleTimer = join42(homedir10(), ".config", "systemd", "user", `${label}.timer`);
19537
- if (existsSync38(staleTimer)) {
19946
+ const staleTimer = join43(homedir10(), ".config", "systemd", "user", `${label}.timer`);
19947
+ if (existsSync39(staleTimer)) {
19538
19948
  process.stdout.write(
19539
19949
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
19540
19950
  `
@@ -19561,7 +19971,7 @@ if (command === "passenger-sync-daemon") {
19561
19971
  const tracker = new Tracker(c, cfg.ship);
19562
19972
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
19563
19973
  for (const r of await resolvedRepos(c)) {
19564
- if (!existsSync38(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
19974
+ if (!existsSync39(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
19565
19975
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
19566
19976
  readiness.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
19567
19977
  }
@@ -19579,7 +19989,7 @@ if (command === "passenger-sync-daemon") {
19579
19989
  }
19580
19990
  async function doctorFix(o) {
19581
19991
  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))));
19582
- const unitExists = (p) => existsSync38(p);
19992
+ const unitExists = (p) => existsSync39(p);
19583
19993
  const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
19584
19994
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
19585
19995
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -20142,7 +20552,7 @@ ${cu.tail.trimEnd()}
20142
20552
  case "run": {
20143
20553
  const skipInlineRelease = flag("no-release");
20144
20554
  if (!dryRun) {
20145
- for (const r of fleetWide ? cfg.routes : [route]) {
20555
+ for (const r of fleetWide ? cfg.routes : [route2]) {
20146
20556
  await reconcileRouteShipName({ route: r, ship: cfg.ship, configFile: cfg.configFile, emit: emit.forRoute(r.route) });
20147
20557
  }
20148
20558
  }
@@ -20189,8 +20599,8 @@ ${cu.tail.trimEnd()}
20189
20599
  `swept ${r.parked} parked, ${r.restored} restored${r.failed ? `, ${r.failed} failed` : ""}${r.contended ? `, ${r.contended} contended` : ""} (${e.route.route})`,
20190
20600
  { step: "sweep", data: r }
20191
20601
  );
20192
- } catch (err) {
20193
- remit.warn(`could not sweep ${e.route.route}: ${err.message}`, { step: "sweep" });
20602
+ } catch (err2) {
20603
+ remit.warn(`could not sweep ${e.route.route}: ${err2.message}`, { step: "sweep" });
20194
20604
  }
20195
20605
  }
20196
20606
  process.stdout.write(renderFleet(fleet, state));
@@ -20256,11 +20666,11 @@ ${cu.tail.trimEnd()}
20256
20666
  if (w.role === "dev" || w.role === "design" || w.role === "qa") {
20257
20667
  const seat = w.route.resolved?.seats[w.role];
20258
20668
  if (seat) {
20259
- const candidates = rankedCandidates(w.role, w.decision.selectionInput);
20669
+ const candidates2 = rankedCandidates(w.role, w.decision.selectionInput);
20260
20670
  const contract = w.decision.selectionInput.contract ?? DEFAULT_CONTRACT;
20261
20671
  const result = await resolveTopCandidate(
20262
20672
  fleetTracker,
20263
- candidates,
20673
+ candidates2,
20264
20674
  seat,
20265
20675
  claimStatuses(w.role, contract).approved,
20266
20676
  claimStatuses(w.role, contract).building,
@@ -20382,9 +20792,9 @@ ${cu.tail.trimEnd()}
20382
20792
  if (!skipInlineRelease) await releaseFleet();
20383
20793
  break;
20384
20794
  }
20385
- if (!await anyRepoServable(route)) {
20795
+ if (!await anyRepoServable(route2)) {
20386
20796
  emit.error(
20387
- `refusing this route \u2014 this ship is ${cfg.ship.platform}, but ${await explainUnservable(route)}`
20797
+ `refusing this route \u2014 this ship is ${cfg.ship.platform}, but ${await explainUnservable(route2)}`
20388
20798
  );
20389
20799
  process.exit(1);
20390
20800
  }
@@ -20392,10 +20802,10 @@ ${cu.tail.trimEnd()}
20392
20802
  emit.emit("the crew is paused");
20393
20803
  break;
20394
20804
  }
20395
- const decision = await decideCycle({ route, ship: cfg.ship, state, emit });
20805
+ const decision = await decideCycle({ route: route2, ship: cfg.ship, state, emit });
20396
20806
  if (!dryRun) {
20397
- state.attention(route.route).persist(decision.attention);
20398
- state.unroutable(route.route).persist(decision.unroutable);
20807
+ state.attention(route2.route).persist(decision.attention);
20808
+ state.unroutable(route2.route).persist(decision.unroutable);
20399
20809
  }
20400
20810
  if (decision.sweep.length) {
20401
20811
  if (dryRun) {
@@ -20406,8 +20816,8 @@ ${cu.tail.trimEnd()}
20406
20816
  });
20407
20817
  }
20408
20818
  } else {
20409
- const seat = route.resolved?.seats.qa ?? route.resolved?.seats.dev;
20410
- const r = await applySweep(new Tracker(route, cfg.ship), decision.sweep, seat ?? "", emit);
20819
+ const seat = route2.resolved?.seats.qa ?? route2.resolved?.seats.dev;
20820
+ const r = await applySweep(new Tracker(route2, cfg.ship), decision.sweep, seat ?? "", emit);
20411
20821
  emit.emit(
20412
20822
  `swept ${r.parked} parked, ${r.restored} restored${r.failed ? `, ${r.failed} failed` : ""}${r.contended ? `, ${r.contended} contended` : ""}`,
20413
20823
  { step: "sweep", data: r }
@@ -20418,19 +20828,19 @@ ${cu.tail.trimEnd()}
20418
20828
  if (dryRun) {
20419
20829
  for (const s of decision.epicSync) emit.emit(`would move ${describeEpicStep(s)}`, { step: "sweep" });
20420
20830
  } else {
20421
- const tracker = new Tracker(route, cfg.ship);
20831
+ const tracker = new Tracker(route2, cfg.ship);
20422
20832
  await applyEpicSync(tracker, decision.epicSync, tracker.contract, emit);
20423
20833
  }
20424
20834
  }
20425
20835
  if (!dryRun && decision.holdReleases?.length) {
20426
- const tracker = new Tracker(route, cfg.ship);
20836
+ const tracker = new Tracker(route2, cfg.ship);
20427
20837
  await applyHoldReleases(tracker, decision.holdReleases, tracker.contract, emit);
20428
20838
  }
20429
20839
  const role = value("role") ?? decision.selection.selected;
20430
20840
  if (command === "poll") break;
20431
20841
  if (!dryRun) {
20432
20842
  try {
20433
- await new Tracker(route, cfg.ship).beatShip(cfg.ship.name);
20843
+ await new Tracker(route2, cfg.ship).beatShip(cfg.ship.name);
20434
20844
  } catch (e) {
20435
20845
  emit.warn(`could not beat ship: ${e.message}`, { step: "poll" });
20436
20846
  }
@@ -20445,7 +20855,7 @@ ${cu.tail.trimEnd()}
20445
20855
  const explicitRole = value("role") !== void 0;
20446
20856
  const ran = [];
20447
20857
  let current = role;
20448
- const tracker2 = new Tracker(route, cfg.ship);
20858
+ const tracker2 = new Tracker(route2, cfg.ship);
20449
20859
  while (current) {
20450
20860
  if (!dryRun) {
20451
20861
  const got = state.acquireRun(current, cfg.ship.maxConcurrentAgents, cfg.ship.roleConcurrency[current] ?? 1);
@@ -20462,20 +20872,20 @@ ${cu.tail.trimEnd()}
20462
20872
  lock = got;
20463
20873
  }
20464
20874
  let ticketHint = decision.actionable.top?.issue_id;
20465
- let ticketRepo = repoTargetFor(route, decision.actionable.top?.repo_id)?.name;
20875
+ let ticketRepo = repoTargetFor(route2, decision.actionable.top?.repo_id)?.name;
20466
20876
  let workingId = decision.actionable.top?.id ?? null;
20467
20877
  if (!dryRun && (current === "dev" || current === "design" || current === "qa")) {
20468
- const seat = route.resolved?.seats[current];
20878
+ const seat = route2.resolved?.seats[current];
20469
20879
  if (seat) {
20470
- const candidates = rankedCandidates(current, decision.selectionInput);
20880
+ const candidates2 = rankedCandidates(current, decision.selectionInput);
20471
20881
  const contract = decision.selectionInput.contract ?? DEFAULT_CONTRACT;
20472
20882
  const result = await resolveTopCandidate(
20473
20883
  tracker2,
20474
- candidates,
20884
+ candidates2,
20475
20885
  seat,
20476
20886
  claimStatuses(current, contract).approved,
20477
20887
  claimStatuses(current, contract).building,
20478
- (t) => dirForRepo(route, t.repo_id) !== null,
20888
+ (t) => dirForRepo(route2, t.repo_id) !== null,
20479
20889
  withHoldCheck(await tracker2.claimAffinity(), decision.selectionInput),
20480
20890
  (t) => {
20481
20891
  const got = state.acquireTicket(t.id);
@@ -20513,7 +20923,7 @@ ${cu.tail.trimEnd()}
20513
20923
  decision.selectionInput.ships?.rows ?? [],
20514
20924
  cfg.ship.name,
20515
20925
  (t) => {
20516
- const d = dirForRepo(route, t.repo_id);
20926
+ const d = dirForRepo(route2, t.repo_id);
20517
20927
  return d ? originBranchSha(d, t, resolve2) : null;
20518
20928
  }
20519
20929
  );
@@ -20531,11 +20941,11 @@ ${cu.tail.trimEnd()}
20531
20941
  if (result.claimed) {
20532
20942
  const idx = decision.tickets.findIndex((t) => t.id === result.ticket.id);
20533
20943
  if (idx >= 0) decision.tickets[idx] = result.ticket;
20534
- writeDigest({ route, ship: cfg.ship, state, emit }, decision, current, cfg.ship.stateDir);
20944
+ writeDigest({ route: route2, ship: cfg.ship, state, emit }, decision, current, cfg.ship.stateDir);
20535
20945
  }
20536
20946
  ticketHint = result.ticket.issue_id;
20537
20947
  workingId = result.ticket.id;
20538
- const target = repoTargetFor(route, result.ticket.repo_id);
20948
+ const target = repoTargetFor(route2, result.ticket.repo_id);
20539
20949
  ticketRepo = target?.name;
20540
20950
  try {
20541
20951
  if (target && ensureRepoCheckout(target.dir, target.remote)) {
@@ -20556,19 +20966,19 @@ ${cu.tail.trimEnd()}
20556
20966
  }
20557
20967
  const plan = planAgentRun({
20558
20968
  role: current,
20559
- route,
20969
+ route: route2,
20560
20970
  ship: cfg.ship,
20561
20971
  stateDir: cfg.ship.stateDir,
20562
- roster: rosterFor(decision, route, current),
20563
- environment: await environmentFor(route, ticketHint),
20564
- apiKey: resolveApiKey(route),
20972
+ roster: rosterFor(decision, route2, current),
20973
+ environment: await environmentFor(route2, ticketHint),
20974
+ apiKey: resolveApiKey(route2),
20565
20975
  cycle: emit.cycle,
20566
20976
  ticket: ticketHint,
20567
20977
  repo: ticketRepo,
20568
- ...personaPromptOptions(await fetchPersonaPrompt(route, current, { userAgent: cfg.ship.userAgent })),
20569
- skillOverrides: await fetchSkillOverrides(route, { userAgent: cfg.ship.userAgent }),
20570
- agentPersona: await fetchSeatPersona(route, current, { userAgent: cfg.ship.userAgent }),
20571
- resolvedAgentId: await resolveAgentId(route, current, { userAgent: cfg.ship.userAgent })
20978
+ ...personaPromptOptions(await fetchPersonaPrompt(route2, current, { userAgent: cfg.ship.userAgent })),
20979
+ skillOverrides: await fetchSkillOverrides(route2, { userAgent: cfg.ship.userAgent }),
20980
+ agentPersona: await fetchSeatPersona(route2, current, { userAgent: cfg.ship.userAgent }),
20981
+ resolvedAgentId: await resolveAgentId(route2, current, { userAgent: cfg.ship.userAgent })
20572
20982
  });
20573
20983
  if (dryRun) {
20574
20984
  process.stdout.write(`${describePlan(plan)}
@@ -20576,7 +20986,7 @@ ${cu.tail.trimEnd()}
20576
20986
  if (workingId && (current === "dev" || current === "design")) {
20577
20987
  try {
20578
20988
  const t = await tracker2.ticket(workingId);
20579
- const r = repoConfigFor(route, t);
20989
+ const r = repoConfigFor(route2, t);
20580
20990
  const would = r && describeHandoffPush(r.target.dir, r.cfg, t, current);
20581
20991
  if (would) process.stdout.write(`${would}
20582
20992
  `);
@@ -20588,7 +20998,7 @@ ${cu.tail.trimEnd()}
20588
20998
  const before = snapshot(await tracker2.openTickets());
20589
20999
  emit.enter("agent", current);
20590
21000
  emit.emit("starting agent run");
20591
- const memberId = route.resolved?.seats[current];
21001
+ const memberId = route2.resolved?.seats[current];
20592
21002
  if (memberId) {
20593
21003
  try {
20594
21004
  await tracker2.setCrewStatus(memberId, "working", workingId);
@@ -20597,20 +21007,20 @@ ${cu.tail.trimEnd()}
20597
21007
  }
20598
21008
  }
20599
21009
  try {
20600
- await tracker2.beatEngaged(cfg.ship.name, route.route, workingId);
21010
+ await tracker2.beatEngaged(cfg.ship.name, route2.route, workingId);
20601
21011
  } catch (e) {
20602
21012
  emit.warn(`could not beat ship engaged: ${e.message}`, { step: "agent" });
20603
21013
  }
20604
21014
  try {
20605
21015
  const runStart = (/* @__PURE__ */ new Date()).toISOString();
20606
21016
  await spawnAgent(plan, emit);
20607
- await guardRunHandoff(route, cfg.ship, tracker2, current, workingId, emit, runStart);
21017
+ await guardRunHandoff(route2, cfg.ship, tracker2, current, workingId, emit, runStart);
20608
21018
  } finally {
20609
21019
  dropLock();
20610
21020
  await settleRunEnd({
20611
21021
  tracker: tracker2,
20612
21022
  shipName: cfg.ship.name,
20613
- route,
21023
+ route: route2,
20614
21024
  role: current,
20615
21025
  memberId,
20616
21026
  ownTicketRecordId: workingId,
@@ -20693,21 +21103,43 @@ ${cu.tail.trimEnd()}
20693
21103
  process.stderr.write("crew: daemon does not support --dry-run \u2014 it only ever performs real cycles.\n");
20694
21104
  process.exit(2);
20695
21105
  }
20696
- if (!await anyRepoServable(route)) {
21106
+ if (!await anyRepoServable(route2)) {
20697
21107
  emit.error(
20698
- `refusing this route \u2014 this ship is ${cfg.ship.platform}, but ${await explainUnservable(route)}`
21108
+ `refusing this route \u2014 this ship is ${cfg.ship.platform}, but ${await explainUnservable(route2)}`
20699
21109
  );
20700
21110
  process.exit(1);
20701
21111
  }
20702
21112
  requireArmed("run the daemon loop");
20703
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join42(CREW_HOME, "src"), join42(CREW_HOME, "dist"), join42(CREW_HOME, "bin")];
21113
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join43(CREW_HOME, "src"), join43(CREW_HOME, "dist"), join43(CREW_HOME, "bin")];
20704
21114
  const configPaths = daemonConfigPaths(cfg);
20705
21115
  watchPaths.push(...configPaths);
20706
21116
  noteDaemonStarted(cfg.ship.stateDir);
20707
21117
  const spawnBreaker = new SpawnBreaker();
21118
+ const controlRoutes = cfg.routes.filter((r) => r.resolved?.workspaceId);
21119
+ const controlServer = await startControlServer({
21120
+ stateDir: cfg.ship.stateDir,
21121
+ state,
21122
+ auth: new ControlAuth({
21123
+ baseUrl: route2.baseUrl || DEFAULT_BASE_URL,
21124
+ workspaceIds: [...new Set(controlRoutes.map((r) => r.resolved.workspaceId))],
21125
+ shipKeys: () => cfg.routes.flatMap((r) => {
21126
+ try {
21127
+ return [resolveApiKey(r)];
21128
+ } catch {
21129
+ return [];
21130
+ }
21131
+ }),
21132
+ userAgent: cfg.ship.userAgent
21133
+ }),
21134
+ status: () => crewJson(["status", route2.route, "--json"]),
21135
+ doctor: () => crewJson(["doctor", route2.route, "--json", "--offline"]),
21136
+ scheduler: schedulerHandler(planDaemonControl(CREW_HOME, hostPlatform(), detectSystemd())),
21137
+ log: (m) => emit.warn(m)
21138
+ });
21139
+ if (controlServer) emit.emit(`control API listening on ${controlServer.path}`);
20708
21140
  const stopReason = await runDaemonLoop({
20709
21141
  runPass: () => runOnePass({
20710
- route,
21142
+ route: route2,
20711
21143
  ship: cfg.ship,
20712
21144
  state,
20713
21145
  maxConcurrentAgents: cfg.ship.maxConcurrentAgents,
@@ -20715,7 +21147,7 @@ ${cu.tail.trimEnd()}
20715
21147
  spawnBreaker,
20716
21148
  configFile: cfg.configFile,
20717
21149
  newEmitter: () => new Emitter({
20718
- route: route.route,
21150
+ route: route2.route,
20719
21151
  eventFile: eventFileFor(cfg.ship.stateDir),
20720
21152
  rotateBytes: cfg.ship.eventsRotateBytes,
20721
21153
  logFile: cfg.ship.logFile,
@@ -20723,13 +21155,13 @@ ${cu.tail.trimEnd()}
20723
21155
  `)
20724
21156
  })
20725
21157
  }),
20726
- onTransient: ({ err, consecutive, backoffMs }) => emit.warn(
20727
- `tracker unavailable (${describeCause(err)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
21158
+ onTransient: ({ err: err2, consecutive, backoffMs }) => emit.warn(
21159
+ `tracker unavailable (${describeCause(err2)}); attempt ${consecutive}, retrying in ${Math.round(backoffMs / 1e3)}s`
20728
21160
  ),
20729
- onOutage: ({ err, downMs }) => {
20730
- const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err)}`;
21161
+ onOutage: ({ err: err2, downMs }) => {
21162
+ const msg = `tracker unreachable for ${Math.max(1, Math.round(downMs / 6e4))} minutes: ${describeCause(err2)}`;
20731
21163
  emit.error(msg);
20732
- void raiseShipAttention({ state, emit, route, ship: cfg.ship }, {
21164
+ void raiseShipAttention({ state, emit, route: route2, ship: cfg.ship }, {
20733
21165
  kind: "tracker_unreachable",
20734
21166
  key: TRACKER_OUTAGE_KEY,
20735
21167
  message: msg,
@@ -20740,8 +21172,8 @@ ${cu.tail.trimEnd()}
20740
21172
  clearShipAttention(state, TRACKER_OUTAGE_KEY);
20741
21173
  emit.emit(`tracker is back after ${Math.max(1, Math.round(downMs / 1e3))}s`);
20742
21174
  },
20743
- onPassError: (err) => emit.error(
20744
- `daemon pass failed, continuing: ${err instanceof Error ? err.stack ?? err.message : String(err)}`
21175
+ onPassError: (err2) => emit.error(
21176
+ `daemon pass failed, continuing: ${err2 instanceof Error ? err2.stack ?? err2.message : String(err2)}`
20745
21177
  ),
20746
21178
  checkStale: makeStaleChecker(watchPaths, { reloadSentinel: reloadSentinelFor(cfg.ship.stateDir) }),
20747
21179
  onPass: (result) => {
@@ -20749,6 +21181,7 @@ ${cu.tail.trimEnd()}
20749
21181
  for (const s of result.skipped) emit.emit(`daemon pass skipped ${s.role} \u2014 ${s.reason}`);
20750
21182
  }
20751
21183
  });
21184
+ await controlServer?.close();
20752
21185
  if (stopReason === "stale") {
20753
21186
  emit.emit(
20754
21187
  "crew daemon: installed tree, crew.yaml or a reload request changed on disk \u2014 exiting cleanly so the OS-level restart relaunches with the update"
@@ -20777,7 +21210,7 @@ ${cu.tail.trimEnd()}
20777
21210
  role: value("role"),
20778
21211
  ticket: value("ticket"),
20779
21212
  level: value("level"),
20780
- route: positional[1] ? route.route : void 0
21213
+ route: positional[1] ? route2.route : void 0
20781
21214
  }
20782
21215
  });
20783
21216
  process.on("SIGINT", () => {
@@ -20929,7 +21362,7 @@ ${cu.tail.trimEnd()}
20929
21362
  `);
20930
21363
  if (!dryRun) {
20931
21364
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
20932
- const raw = existsSync38(resolvedPath) ? JSON.parse(readFileSync34(resolvedPath, "utf8")) : {};
21365
+ const raw = existsSync39(resolvedPath) ? JSON.parse(readFileSync34(resolvedPath, "utf8")) : {};
20933
21366
  raw.agentSkills = result.agentSkills;
20934
21367
  mkdirSync25(dirOf2(resolvedPath), { recursive: true });
20935
21368
  writeFileSync26(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -21009,7 +21442,7 @@ ${cu.tail.trimEnd()}
21009
21442
  const found = cfg.routes.find((r) => r.route === routeArg);
21010
21443
  if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
21011
21444
  const named2 = value("route") ? void 0 : positional[3];
21012
- const dir = named2 ? repoDirOf(configText, dirname21(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
21445
+ const dir = named2 ? repoDirOf(configText, dirname22(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
21013
21446
  const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
21014
21447
  const o = target ? found.repoOverrides[target.name] : void 0;
21015
21448
  runReposCheck(dir, {
@@ -21032,11 +21465,11 @@ ${cu.tail.trimEnd()}
21032
21465
  if (sub === "hooks") {
21033
21466
  try {
21034
21467
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
21035
- const dir = repoDirOf(configText, dirname21(cfg.configFile), a.route, a.name);
21036
- if (!existsSync38(dir)) throw new ReposError(`${dir} does not exist on disk`);
21037
- const files = { exists: (r) => existsSync38(resolvePath(dir, r)), list: (r) => {
21468
+ const dir = repoDirOf(configText, dirname22(cfg.configFile), a.route, a.name);
21469
+ if (!existsSync39(dir)) throw new ReposError(`${dir} does not exist on disk`);
21470
+ const files = { exists: (r) => existsSync39(resolvePath(dir, r)), list: (r) => {
21038
21471
  try {
21039
- return readdirSync17(resolvePath(dir, r));
21472
+ return readdirSync18(resolvePath(dir, r));
21040
21473
  } catch {
21041
21474
  return [];
21042
21475
  }
@@ -21152,7 +21585,7 @@ ${cu.tail.trimEnd()}
21152
21585
  if (sub === "list" && !routeName) usage2();
21153
21586
  if (sub === "add") {
21154
21587
  try {
21155
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync38(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
21588
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync39(resolvePath(p.replace(/^~(?=\/|$)/, homedir10()))));
21156
21589
  routeName = a.route;
21157
21590
  addPath = a.path;
21158
21591
  addName = a.name;
@@ -21181,7 +21614,7 @@ ${cu.tail.trimEnd()}
21181
21614
  }
21182
21615
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
21183
21616
  let resolvedRemotes;
21184
- if (existsSync38(resolvedFile)) {
21617
+ if (existsSync39(resolvedFile)) {
21185
21618
  try {
21186
21619
  resolvedRemotes = JSON.parse(readFileSync34(resolvedFile, "utf8"));
21187
21620
  } catch {
@@ -21189,8 +21622,8 @@ ${cu.tail.trimEnd()}
21189
21622
  }
21190
21623
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
21191
21624
  const text = configText;
21192
- const base = dirname21(cfg.configFile);
21193
- const isCheckout = (d) => existsSync38(`${d}/.git`);
21625
+ const base = dirname22(cfg.configFile);
21626
+ const isCheckout = (d) => existsSync39(`${d}/.git`);
21194
21627
  try {
21195
21628
  if (sub === "list") {
21196
21629
  if (flag("json")) {
@@ -21200,7 +21633,7 @@ ${cu.tail.trimEnd()}
21200
21633
  base,
21201
21634
  resolved: resolvedRemotes,
21202
21635
  originOf,
21203
- exists: (d) => existsSync38(d),
21636
+ exists: (d) => existsSync39(d),
21204
21637
  isCheckout,
21205
21638
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
21206
21639
  try {
@@ -21221,7 +21654,7 @@ ${cu.tail.trimEnd()}
21221
21654
  for (const r of repos) {
21222
21655
  const origin2 = originOf(r.dir);
21223
21656
  const row = matchRepoName(origin2, resolvedRemotes);
21224
- const note = !existsSync38(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
21657
+ const note = !existsSync39(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
21225
21658
  process.stdout.write(`${r.name} ${r.dir} ${note}
21226
21659
  `);
21227
21660
  }
@@ -21233,9 +21666,9 @@ ${cu.tail.trimEnd()}
21233
21666
  ` : `${human}
21234
21667
  `);
21235
21668
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
21236
- if (existsSync38(plan2.target) && (existsSync38(`${plan2.target}/.git`) || readdirSync17(plan2.target).length > 0)) {
21669
+ if (existsSync39(plan2.target) && (existsSync39(`${plan2.target}/.git`) || readdirSync18(plan2.target).length > 0)) {
21237
21670
  throw new ReposError(
21238
- existsSync38(`${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`
21671
+ existsSync39(`${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`
21239
21672
  );
21240
21673
  }
21241
21674
  if (dryRun) {
@@ -21260,7 +21693,7 @@ ${cu.tail.trimEnd()}
21260
21693
  break;
21261
21694
  }
21262
21695
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir10()));
21263
- const top = existsSync38(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
21696
+ const top = existsSync39(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
21264
21697
  if (!top) throw new ReposError(`${target} is not a git checkout`);
21265
21698
  const origin = originOf(top);
21266
21699
  const plan = planRepoAdd({
@@ -21312,7 +21745,7 @@ ${cu.tail.trimEnd()}
21312
21745
  const created = await rowTracker.createRepoRow(rowPayload);
21313
21746
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
21314
21747
  `);
21315
- if (existsSync38(resolvedFile)) {
21748
+ if (existsSync39(resolvedFile)) {
21316
21749
  const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
21317
21750
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
21318
21751
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
@@ -21693,11 +22126,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21693
22126
  if (found.shipRow) {
21694
22127
  connectEvents.emit("ship_row", `Ships row "${found.shipRow.name}" ${found.shipRow.action}`, found.shipRow);
21695
22128
  }
21696
- const route2 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
21697
- const resolvedPath = resolvedPathFor(cfg.ship.stateDir, route2);
22129
+ const route3 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
22130
+ const resolvedPath = resolvedPathFor(cfg.ship.stateDir, route3);
21698
22131
  const capabilities = await checkCapabilities(authOpts, found);
21699
22132
  if (flag("json")) {
21700
- process.stdout.write(`${JSON.stringify({ event: "capabilities", route: route2, items: capabilities })}
22133
+ process.stdout.write(`${JSON.stringify({ event: "capabilities", route: route3, items: capabilities })}
21701
22134
  `);
21702
22135
  } else {
21703
22136
  process.stderr.write("capabilities for this key:\n");
@@ -21706,10 +22139,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21706
22139
  }
21707
22140
  const capabilityMisses = capabilities.filter((i) => i.status === "missing").map(missLine);
21708
22141
  if (found.locksModelId && !dryRun) {
21709
- const configured = cfg.routes.find((r) => r.route === route2);
22142
+ const configured = cfg.routes.find((r) => r.route === route3);
21710
22143
  const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
21711
22144
  try {
21712
- const made = await provisionReleaseLocks(authOpts, found.locksModelId, route2, repoNames);
22145
+ const made = await provisionReleaseLocks(authOpts, found.locksModelId, route3, repoNames);
21713
22146
  for (const sc of made) process.stderr.write(`created release lock row ${sc}
21714
22147
  `);
21715
22148
  } catch (e) {
@@ -21730,7 +22163,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21730
22163
  }
21731
22164
  }
21732
22165
  let previous;
21733
- const hadPreviousFile = existsSync38(resolvedPath);
22166
+ const hadPreviousFile = existsSync39(resolvedPath);
21734
22167
  if (hadPreviousFile) {
21735
22168
  try {
21736
22169
  previous = JSON.parse(readFileSync34(resolvedPath, "utf8"));
@@ -21785,7 +22218,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21785
22218
  }
21786
22219
  if (!operator && !hadPreviousFile && !operatorWouldBeCreated) {
21787
22220
  process.stderr.write(
21788
- `crew connect ${route2}: cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it, none matching this key's own email, and there's no terminal to ask.
22221
+ `crew connect ${route3}: cannot determine the operator \u2014 ${found.candidates.length} Crew row(s) could be it, none matching this key's own email, and there's no terminal to ask.
21789
22222
  Re-run from a real terminal to pick one, or pass --key for a key whose own
21790
22223
  identity email matches exactly one Crew row.
21791
22224
  `
@@ -21862,7 +22295,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21862
22295
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
21863
22296
  `);
21864
22297
  } else {
21865
- mkdirSync25(dirname21(resolvedPath), { recursive: true });
22298
+ mkdirSync25(dirname22(resolvedPath), { recursive: true });
21866
22299
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
21867
22300
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
21868
22301
  writeFileSync26(tmpPath, `${JSON.stringify(resolved, null, 2)}
@@ -21906,12 +22339,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21906
22339
  stopWithProblem(msg, "wizard");
21907
22340
  }
21908
22341
  if ((mintedFromDevice || firstRunFromFlags) && !keychainBacked) {
21909
- apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route2);
22342
+ apiKeyFile = apiKeyPathFor(cfg.ship.stateDir, route3);
21910
22343
  if (dryRun) {
21911
22344
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
21912
22345
  `);
21913
22346
  } else {
21914
- mkdirSync25(dirname21(apiKeyFile), { recursive: true });
22347
+ mkdirSync25(dirname22(apiKeyFile), { recursive: true });
21915
22348
  writeFileSync26(apiKeyFile, `${apiKeyVar}=${apiKey}
21916
22349
  `, { mode: 384 });
21917
22350
  process.stderr.write(`API key written to ${apiKeyFile}
@@ -21924,7 +22357,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
21924
22357
  const routeDir = value("dir");
21925
22358
  const routeBlock = renderConnection(
21926
22359
  found,
21927
- route2,
22360
+ route3,
21928
22361
  routeDir ?? (writeFirstRun ? null : "REPLACE \u2014 the local checkout this route works"),
21929
22362
  {
21930
22363
  area: value("area"),
@@ -22006,7 +22439,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22006
22439
  }
22007
22440
  if (wroteConfig) {
22008
22441
  process.stderr.write(nextSteps({
22009
- route: route2,
22442
+ route: route3,
22010
22443
  configPath: wizard.configPath,
22011
22444
  platform: cfg.ship.platform,
22012
22445
  needsRepos: !value("dir")
@@ -22016,14 +22449,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22016
22449
  } else if (operator) {
22017
22450
  process.stderr.write(
22018
22451
  `
22019
- Paste the block above under routes: in crew.yaml (if "${route2}" isn't there already), then: crew doctor ${route2}
22452
+ Paste the block above under routes: in crew.yaml (if "${route3}" isn't there already), then: crew doctor ${route3}
22020
22453
  `
22021
22454
  );
22022
22455
  } else {
22023
22456
  process.stderr.write(
22024
22457
  `
22025
- Paste the block above under routes: in crew.yaml (if "${route2}" isn't there
22026
- already), set operator in ${resolvedPath}, then: crew doctor ${route2}
22458
+ Paste the block above under routes: in crew.yaml (if "${route3}" isn't there
22459
+ already), set operator in ${resolvedPath}, then: crew doctor ${route3}
22027
22460
  `
22028
22461
  );
22029
22462
  }
@@ -22032,12 +22465,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22032
22465
  process.stderr.write(`
22033
22466
  agents sync: skipped (${why})
22034
22467
  `);
22035
- connectEvents.emit("agents_synced", `agents sync skipped: ${why}`, { route: route2, skipped: why });
22468
+ connectEvents.emit("agents_synced", `agents sync skipped: ${why}`, { route: route3, skipped: why });
22036
22469
  };
22037
22470
  let syncRoute;
22038
22471
  try {
22039
22472
  const reloaded = loadConfig(CREW_HOME, wizard?.configPath ?? cfg.configFile, { allowNoRoutes: true });
22040
- syncRoute = reloaded.routes.find((r) => r.route === route2);
22473
+ syncRoute = reloaded.routes.find((r) => r.route === route3);
22041
22474
  if (syncRoute) await hydrateApiKeys([syncRoute]);
22042
22475
  } catch (e) {
22043
22476
  skip(`config did not load: ${e.message}`);
@@ -22065,24 +22498,24 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22065
22498
  process.stderr.write(missing ? " A workspace admin has to open the Crew app or run `crew agents sync` once before this ship's seats can run.\n" : " Seats are linked to the workspace's existing personas.\n");
22066
22499
  }
22067
22500
  connectEvents.emit("agents_synced", run2.readOnly ? "agents linked (read-only key)" : "agents synced", {
22068
- route: route2,
22501
+ route: route3,
22069
22502
  admin: !run2.readOnly,
22070
22503
  personas: run2.personaResult.outcomes,
22071
22504
  skills: run2.skillResult?.outcomes ?? [],
22072
22505
  diverged: run2.diverged
22073
22506
  });
22074
- for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route2, ...m });
22507
+ for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route3, ...m });
22075
22508
  if (run2.diverged) process.exitCode = 1;
22076
22509
  } catch (e) {
22077
22510
  if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
22078
22511
  else throw e;
22079
22512
  }
22080
22513
  } else if (!syncRoute) {
22081
- skip(`"${route2}" is not in crew.yaml yet; add the block above, then run \`crew agents sync\``);
22514
+ skip(`"${route3}" is not in crew.yaml yet; add the block above, then run \`crew agents sync\``);
22082
22515
  }
22083
22516
  }
22084
- connectEvents.emit("done", `connected ${route2}`, {
22085
- route: route2,
22517
+ connectEvents.emit("done", `connected ${route3}`, {
22518
+ route: route3,
22086
22519
  resolvedPath: dryRun ? void 0 : resolvedPath,
22087
22520
  configPath: wroteConfig ? wizard.configPath : void 0,
22088
22521
  wroteConfig,
@@ -22196,7 +22629,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22196
22629
  break;
22197
22630
  }
22198
22631
  case "ports": {
22199
- const where = await worktreeLocations(route);
22632
+ const where = await worktreeLocations(route2);
22200
22633
  const rows = listeners().map((l) => ({ ...l, t: ticketForPort(l.port) })).filter((r) => r.t).sort((a, b) => a.port - b.port);
22201
22634
  if (!rows.length) {
22202
22635
  process.stdout.write("nothing of ours is listening\n");
@@ -22214,7 +22647,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22214
22647
  case "reap": {
22215
22648
  emit.enter("worktree");
22216
22649
  let anything = false;
22217
- const orphans = findOrphansIn(await worktreeLocations(route));
22650
+ const orphans = findOrphansIn(await worktreeLocations(route2));
22218
22651
  for (const o of orphans) {
22219
22652
  anything = true;
22220
22653
  emit.emit(
@@ -22222,12 +22655,12 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22222
22655
  );
22223
22656
  }
22224
22657
  if (!dryRun && orphans.length) await killGently(orphans.map((o) => o.pid));
22225
- const tracker = new Tracker(route, cfg.ship);
22658
+ const tracker = new Tracker(route2, cfg.ship);
22226
22659
  const truncated = (limit) => emit.warn(`ticket lookup for the sweep hit its ${limit}-row limit; some tickets may be missed`, { step: "worktree" });
22227
- for (const r of await resolvedRepos(route)) {
22228
- const wtTickets = ticketsByRepo(route, await tracker.ticketsByNumbers(worktreeTicketNumbers(r), truncated)).byRepo.get(r.name) ?? [];
22660
+ for (const r of await resolvedRepos(route2)) {
22661
+ const wtTickets = ticketsByRepo(route2, await tracker.ticketsByNumbers(worktreeTicketNumbers(r), truncated)).byRepo.get(r.name) ?? [];
22229
22662
  const brTickets = ticketsByRepo(
22230
- route,
22663
+ route2,
22231
22664
  await tracker.ticketsByNumbers(remoteBranchTicketNumbers(r.dir, r.config.branch.remote, r.config.branch.base), truncated)
22232
22665
  ).byRepo.get(r.name) ?? [];
22233
22666
  const actions = planWorktreeSweep(r, wtTickets, tracker.contract);
@@ -22282,26 +22715,26 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22282
22715
  process.exit(2);
22283
22716
  }
22284
22717
  const num = Number(n);
22285
- const repos = await resolvedRepos(route);
22286
- const candidates = repos.map((r) => ({ repo: r, found: worktreeForNumber(r.dir, n) })).filter((c) => c.found !== null);
22287
- if (candidates.length === 0) {
22718
+ const repos = await resolvedRepos(route2);
22719
+ const candidates2 = repos.map((r) => ({ repo: r, found: worktreeForNumber(r.dir, n) })).filter((c) => c.found !== null);
22720
+ if (candidates2.length === 0) {
22288
22721
  process.stderr.write(
22289
22722
  `drop: no worktree for ISSUE-${n} in any of this route's repositories (looked in: ${repos.map((r) => r.dir).join(", ")})
22290
22723
  `
22291
22724
  );
22292
22725
  process.exit(2);
22293
22726
  }
22294
- if (candidates.length > 1) {
22727
+ if (candidates2.length > 1) {
22295
22728
  process.stderr.write(
22296
- `drop: ISSUE-${n} has a worktree in more than one repository (${candidates.map((c) => c.repo.name).join(", ")}) \u2014 remove them one at a time by path
22729
+ `drop: ISSUE-${n} has a worktree in more than one repository (${candidates2.map((c) => c.repo.name).join(", ")}) \u2014 remove them one at a time by path
22297
22730
  `
22298
22731
  );
22299
22732
  process.exit(2);
22300
22733
  }
22301
- const { repo: target, found } = candidates[0];
22734
+ const { repo: target, found } = candidates2[0];
22302
22735
  const wt = found.path;
22303
22736
  try {
22304
- const trD = new Tracker(route, cfg.ship);
22737
+ const trD = new Tracker(route2, cfg.ship);
22305
22738
  const tD = (await trD.ticketsByNumbers([n])).find((t) => t.issue_id.replace(/^\D+/, "") === String(num));
22306
22739
  if (tD && tD[trD.contract.columns.status] === trD.contract.statuses.draft) {
22307
22740
  process.stderr.write(`drop: ${tD.issue_tag ?? tD.issue_id} is a draft ticket \u2014 its worktree belongs to a periscope; end it with \`crew periscope down ${num} --keep\` or \`--discard\`
@@ -22340,9 +22773,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22340
22773
  }
22341
22774
  case "rotate-passenger-url": {
22342
22775
  emit.enter("passengers");
22343
- if (!route.hostPassengers) {
22776
+ if (!route2.hostPassengers) {
22344
22777
  process.stderr.write(
22345
- `crew rotate-passenger-url: route "${route.route}" does not have hostPassengers enabled
22778
+ `crew rotate-passenger-url: route "${route2.route}" does not have hostPassengers enabled
22346
22779
  `
22347
22780
  );
22348
22781
  process.exit(2);
@@ -22351,16 +22784,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22351
22784
  process.stderr.write("crew rotate-passenger-url: this ship has no relayHost configured \u2014 nothing to rotate\n");
22352
22785
  process.exit(2);
22353
22786
  }
22354
- const workspaceId = route.resolved?.workspaceId;
22787
+ const workspaceId = route2.resolved?.workspaceId;
22355
22788
  if (!workspaceId) {
22356
- process.stderr.write(`crew rotate-passenger-url: route "${route.route}" has no resolved ids \u2014 run \`crew connect\`
22789
+ process.stderr.write(`crew rotate-passenger-url: route "${route2.route}" has no resolved ids \u2014 run \`crew connect\`
22357
22790
  `);
22358
22791
  process.exit(2);
22359
22792
  }
22360
22793
  const plans = planContainers(cfg).filter((p) => p.workspaceId === workspaceId);
22361
22794
  if (!plans.length) {
22362
22795
  process.stderr.write(
22363
- `crew rotate-passenger-url: no Host Passengers container is planned for "${route.route}" right now
22796
+ `crew rotate-passenger-url: no Host Passengers container is planned for "${route2.route}" right now
22364
22797
  `
22365
22798
  );
22366
22799
  process.exit(2);
@@ -22369,7 +22802,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22369
22802
  const hasLiveTunnel = existing !== void 0 && isPidAlive(existing.pid);
22370
22803
  if (dryRun) {
22371
22804
  emit.emit(
22372
- hasLiveTunnel ? `would kill tunnel pid ${existing.pid} for ${route.route} and reconnect with a fresh public URL` : `would reconnect ${route.route} with a fresh public URL (no live tunnel to kill)`
22805
+ hasLiveTunnel ? `would kill tunnel pid ${existing.pid} for ${route2.route} and reconnect with a fresh public URL` : `would reconnect ${route2.route} with a fresh public URL (no live tunnel to kill)`
22373
22806
  );
22374
22807
  break;
22375
22808
  }
@@ -22381,13 +22814,13 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22381
22814
  }
22382
22815
  }
22383
22816
  const { privateKeyPath } = ensureShipSshKeypair(cfg.ship.stateDir);
22384
- const rotateReconcile = endpointReconcileDeps([route], cfg.ship.relayHost);
22817
+ const rotateReconcile = endpointReconcileDeps([route2], cfg.ship.relayHost);
22385
22818
  const rotatePending = [];
22386
22819
  syncPassengerTunnels(plans, cfg.ship.relayHost, cfg.ship.relayPort, privateKeyPath, cfg.ship.stateDir, /* @__PURE__ */ new Map(), {
22387
- apiKeys: passengerApiKeys([route]),
22820
+ apiKeys: passengerApiKeys([route2]),
22388
22821
  onStatus: (wsId, status2, info) => {
22389
22822
  rotatePending.push(
22390
- new Tracker(route, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
22823
+ new Tracker(route2, cfg.ship).updateTunnelState(cfg.ship.name, { tunnel_status: status2 }).catch((e) => {
22391
22824
  emit.warn(`could not write tunnel_status: ${e.message}`, { step: "passengers" });
22392
22825
  })
22393
22826
  );
@@ -22395,7 +22828,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22395
22828
  }
22396
22829
  });
22397
22830
  await Promise.all(rotatePending);
22398
- emit.emit(`rotated the passenger tunnel for ${route.route} \u2014 it reconnects under a fresh slug and the registered endpoint follows on the next passengers cycle`);
22831
+ emit.emit(`rotated the passenger tunnel for ${route2.route} \u2014 it reconnects under a fresh slug and the registered endpoint follows on the next passengers cycle`);
22399
22832
  break;
22400
22833
  }
22401
22834
  case "worktree": {
@@ -22406,7 +22839,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22406
22839
  process.stderr.write("worktree: need a ticket number (326, ISSUE-326, or TABL-326)\n");
22407
22840
  process.exit(2);
22408
22841
  }
22409
- const candidatesWt = !named && cfg.routes.length > 1 ? cfg.routes.filter((r) => r.enabled) : [route];
22842
+ const candidatesWt = !named && cfg.routes.length > 1 ? cfg.routes.filter((r) => r.enabled) : [route2];
22410
22843
  let ticketWt;
22411
22844
  const lookedWt = [];
22412
22845
  const hitsWt = /* @__PURE__ */ new Map();
@@ -22426,7 +22859,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22426
22859
  `);
22427
22860
  process.exit(2);
22428
22861
  }
22429
- const routeWt = candidatesWt.find((r) => r.route === pickedWt.route) ?? route;
22862
+ const routeWt = candidatesWt.find((r) => r.route === pickedWt.route) ?? route2;
22430
22863
  ticketWt = hitsWt.get(pickedWt.route);
22431
22864
  if (!ticketWt) process.exit(2);
22432
22865
  const repoWt = (await resolvedRepos(routeWt)).find((r) => r.id === ticketWt.repo_id) ?? (await resolvedRepos(routeWt))[0];
@@ -22646,7 +23079,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
22646
23079
  const shellR = hit && resolveShell(hit.repo.config.shell ?? cfg.ship.shell);
22647
23080
  const resumed = await runPeriscopeResume({
22648
23081
  find: async () => hit && { row: hit.row, tag: hit.tag },
22649
- worktreeExists: (p) => existsSync38(p),
23082
+ worktreeExists: (p) => existsSync39(p),
22650
23083
  caps: periscopeCaps(cfg.ship),
22651
23084
  runHook: async (name, cwd, env) => {
22652
23085
  const script = hit?.repo.config.hooks[name];
@@ -22740,10 +23173,10 @@ ${where.join("\n")}`);
22740
23173
  },
22741
23174
  prepare: async (path) => {
22742
23175
  for (const f of repoP.config.worktrees.copy) {
22743
- const src = join42(repoP.dir, f);
22744
- if (!existsSync38(src)) continue;
22745
- mkdirSync25(dirOf2(join42(path, f)), { recursive: true });
22746
- copyFileSync2(src, join42(path, f));
23176
+ const src = join43(repoP.dir, f);
23177
+ if (!existsSync39(src)) continue;
23178
+ mkdirSync25(dirOf2(join43(path, f)), { recursive: true });
23179
+ copyFileSync2(src, join43(path, f));
22747
23180
  }
22748
23181
  const setup = repoP.config.hooks.setup;
22749
23182
  return setup ? runScript(setup, { cwd: path, shell: shellP, onLine: (l) => {
@@ -22852,13 +23285,13 @@ ${where.join("\n")}`);
22852
23285
  process.stderr.write("unassign: need a ticket number (326, ISSUE-326, or TABL-326)\n");
22853
23286
  process.exit(2);
22854
23287
  }
22855
- const tracker3 = new Tracker(route, cfg.ship);
23288
+ const tracker3 = new Tracker(route2, cfg.ship);
22856
23289
  const c = tracker3.contract;
22857
23290
  const key = `ISSUE-${n}`;
22858
23291
  const open = await tracker3.openTickets();
22859
23292
  const ticket = open.find((t) => t[c.columns.key] === key);
22860
23293
  if (!ticket) {
22861
- process.stderr.write(`unassign: no open ticket ${key} on ${route.route} (already resolved, or not this route's)
23294
+ process.stderr.write(`unassign: no open ticket ${key} on ${route2.route} (already resolved, or not this route's)
22862
23295
  `);
22863
23296
  process.exit(2);
22864
23297
  }
@@ -22892,10 +23325,10 @@ ${where.join("\n")}`);
22892
23325
  let baseChecked = 0;
22893
23326
  let baseActed = 0;
22894
23327
  let baseUnsafe = 0;
22895
- for (const r of await resolvedRepos(route)) {
23328
+ for (const r of await resolvedRepos(route2)) {
22896
23329
  const base = refreshBaseBranch(r.dir, r.config.branch.remote, r.config.branch.base, dryRun);
22897
23330
  const att = dryRun ? null : baseAttention(r.name, base, r.config.branch.remote, r.config.branch.base);
22898
- if (att && "raise" in att) await raiseShipAttention({ state, emit, route, ship: cfg.ship }, att.raise);
23331
+ if (att && "raise" in att) await raiseShipAttention({ state, emit, route: route2, ship: cfg.ship }, att.raise);
22899
23332
  else if (att) clearShipAttention(state, att.clear);
22900
23333
  if (base.action !== "not-applicable") {
22901
23334
  baseChecked++;
@@ -23133,13 +23566,13 @@ ${where.join("\n")}`);
23133
23566
  `);
23134
23567
  return process.exit(2);
23135
23568
  };
23136
- if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
23137
- const tracker = new Tracker(route, cfg.ship);
23569
+ if (!route2.resolved) fail(`route ${route2.route} is not connected \u2014 run \`crew connect\` first`);
23570
+ const tracker = new Tracker(route2, cfg.ship);
23138
23571
  const configText = readFileSync34(cfg.configFile, "utf8");
23139
- const cfgBase = dirname21(cfg.configFile);
23140
- const configured = configuredRepos(configText, route.route, cfgBase);
23141
- const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
23142
- const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
23572
+ const cfgBase = dirname22(cfg.configFile);
23573
+ const configured = configuredRepos(configText, route2.route, cfgBase);
23574
+ const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route2.reposBasePath, route2.route.split("/")[0]);
23575
+ const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route2.route);
23143
23576
  const rows = await tracker.repoRows();
23144
23577
  const row = rows.find((r) => r.name === SAMPLE_REPO);
23145
23578
  if (flag("remove")) {
@@ -23164,7 +23597,7 @@ ${where.join("\n")}`);
23164
23597
  }
23165
23598
  if (configured.some((r) => r.dir === dir)) {
23166
23599
  try {
23167
- writeFileSync26(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
23600
+ writeFileSync26(cfg.configFile, planRepoRemove(configText, cfgBase, route2.route, dir).text);
23168
23601
  } catch (e) {
23169
23602
  if (e instanceof ReposError) fail(e.message);
23170
23603
  throw e;
@@ -23172,7 +23605,7 @@ ${where.join("\n")}`);
23172
23605
  }
23173
23606
  if (row) {
23174
23607
  await tracker.deleteRepoRow(row.id);
23175
- if (existsSync38(resolvedFile)) {
23608
+ if (existsSync39(resolvedFile)) {
23176
23609
  const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
23177
23610
  if (raw.repoNames) delete raw.repoNames[row.id];
23178
23611
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
@@ -23180,21 +23613,21 @@ ${where.join("\n")}`);
23180
23613
  `);
23181
23614
  }
23182
23615
  }
23183
- if (existsSync38(join42(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync17(dir, { recursive: true, force: true });
23616
+ if (existsSync39(join43(dir, ".git")) && basename13(dir) === SAMPLE_REPO) rmSync18(dir, { recursive: true, force: true });
23184
23617
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
23185
23618
  break;
23186
23619
  }
23187
- const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
23188
- const fresh = !existsSync38(join42(dir, ".git"));
23189
- if (existsSync38(dir) && fresh && readdirSync17(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
23620
+ const projectId = route2.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
23621
+ const fresh = !existsSync39(join43(dir, ".git"));
23622
+ if (existsSync39(dir) && fresh && readdirSync18(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
23190
23623
  if (dryRun) {
23191
23624
  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`);
23192
23625
  break;
23193
23626
  }
23194
23627
  if (fresh) {
23195
23628
  mkdirSync25(dir, { recursive: true });
23196
- writeFileSync26(join42(dir, "README.md"), SAMPLE_README);
23197
- writeFileSync26(join42(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
23629
+ writeFileSync26(join43(dir, "README.md"), SAMPLE_README);
23630
+ writeFileSync26(join43(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
23198
23631
  const g = (...a) => {
23199
23632
  const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
23200
23633
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -23212,7 +23645,7 @@ ${where.join("\n")}`);
23212
23645
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
23213
23646
  });
23214
23647
  repoId = (await tracker.createRepoRow(payload)).id;
23215
- if (existsSync38(resolvedFile)) {
23648
+ if (existsSync39(resolvedFile)) {
23216
23649
  const raw = JSON.parse(readFileSync34(resolvedFile, "utf8"));
23217
23650
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
23218
23651
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
@@ -23222,7 +23655,7 @@ ${where.join("\n")}`);
23222
23655
  }
23223
23656
  if (!configured.some((r) => r.dir === dir)) {
23224
23657
  try {
23225
- writeFileSync26(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
23658
+ writeFileSync26(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route2.route, path: dir, name: SAMPLE_REPO }).text);
23226
23659
  } catch (e) {
23227
23660
  if (e instanceof ReposError) fail(e.message);
23228
23661
  throw e;
@@ -23230,7 +23663,7 @@ ${where.join("\n")}`);
23230
23663
  }
23231
23664
  const ticket = await tracker.fileTicket(
23232
23665
  planSampleTicket({ projectId, repoId, approvedStatus: tracker.contract.statuses.approved }),
23233
- route.resolved.seats.dev ?? ""
23666
+ route2.resolved.seats.dev ?? ""
23234
23667
  );
23235
23668
  const tag = ticket.issue_tag ?? ticket.issue_id;
23236
23669
  say("filed", { ticket: tag, id: ticket.id, dir, repoId }, `filed ${tag} against ${dir}; follow it with \`crew watch\``);
@@ -23245,20 +23678,20 @@ ${where.join("\n")}`);
23245
23678
  `);
23246
23679
  process.exit(2);
23247
23680
  }
23248
- if (!route.resolved) {
23249
- process.stderr.write(`crew selftest: route ${route.route} is not connected \u2014 run \`crew connect\` first
23681
+ if (!route2.resolved) {
23682
+ process.stderr.write(`crew selftest: route ${route2.route} is not connected \u2014 run \`crew connect\` first
23250
23683
  `);
23251
23684
  process.exit(2);
23252
23685
  }
23253
- const seatKey = await resolveSeatKey(route, "dev");
23686
+ const seatKey = await resolveSeatKey(route2, "dev");
23254
23687
  const keychainDown = seatKey.kind === "ship" && /keychain/i.test(seatKey.reason);
23255
23688
  const apiKey = keychainDown ? void 0 : seatKey.apiKey;
23256
- const baseUrl = route.baseUrl || DEFAULT_BASE_URL;
23689
+ const baseUrl = route2.baseUrl || DEFAULT_BASE_URL;
23257
23690
  let workspaceName = "";
23258
23691
  let upfront;
23259
23692
  if (apiKey) {
23260
23693
  try {
23261
- const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/workspaces/${route.resolved.workspaceId}`, {
23694
+ const res = await fetch(`${baseUrl.replace(/\/+$/, "")}/api/workspaces/${route2.resolved.workspaceId}`, {
23262
23695
  headers: { Authorization: `Bearer ${apiKey}`, "User-Agent": cfg.ship.userAgent }
23263
23696
  });
23264
23697
  if (res.status === 401 || res.status === 403) {
@@ -23284,7 +23717,7 @@ ${where.join("\n")}`);
23284
23717
  ship: cfg.ship,
23285
23718
  toolchain: tc,
23286
23719
  baseUrl,
23287
- workspaceId: route.resolved.workspaceId,
23720
+ workspaceId: route2.resolved.workspaceId,
23288
23721
  apiKey,
23289
23722
  // Only the seat's reason: runSelftest appends the remedy itself, as for every other class.
23290
23723
  keyProblem: keychainDown && seatKey.kind === "ship" ? seatKey.reason : void 0,
@@ -23333,25 +23766,25 @@ ${where.join("\n")}`);
23333
23766
  break;
23334
23767
  }
23335
23768
  case "status": {
23336
- const fairness = state.fairness(route.route);
23769
+ const fairness = state.fairness(route2.route);
23337
23770
  const waiting = fairness.waiting();
23338
- const statusTracker = new Tracker(route, cfg.ship);
23771
+ const statusTracker = new Tracker(route2, cfg.ship);
23339
23772
  const shipRows = await statusTracker.shipRows();
23340
23773
  const engagedTicketIds = shipRows.map((s) => s.engaged_ticket_id).filter((id) => !!id);
23341
23774
  const engagedTickets = engagedTicketIds.length ? await statusTracker.ticketsByIds(engagedTicketIds) : [];
23342
23775
  const issueLabelById = new Map(engagedTickets.map((t) => [t.id, t.issue_id]));
23343
- const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route.resolved?.operator ?? "")).catch(() => []);
23776
+ const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route2.resolved?.operator ?? "")).catch(() => []);
23344
23777
  const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
23345
- const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
23778
+ const crewMinVersion = await readCrewFloor(route2, cfg.ship) ?? null;
23346
23779
  const runs = liveRuns(cfg.ship.stateDir);
23347
23780
  const periscopes = await periscopesForStatus(cfg.ship.stateDir);
23348
23781
  if (flag("json")) {
23349
- const nameSync = await reconcileRouteShipName({ route, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
23350
- const repos = await resolvedRepos(route);
23351
- const seatIds = route.resolved?.seats ?? {};
23782
+ const nameSync = await reconcileRouteShipName({ route: route2, ship: cfg.ship, configFile: cfg.configFile, readOnly: true });
23783
+ const repos = await resolvedRepos(route2);
23784
+ const seatIds = route2.resolved?.seats ?? {};
23352
23785
  const crewRows = Object.keys(seatIds).length ? await statusTracker.crewRows() : [];
23353
23786
  const crewRowById = new Map(crewRows.map((c) => [c.id, c]));
23354
- const agentPersonas = route.resolved?.agentPersonas ?? {};
23787
+ const agentPersonas = route2.resolved?.agentPersonas ?? {};
23355
23788
  const seats2 = ROLE_NAMES.filter((r) => r !== "pair").map((r) => {
23356
23789
  const seatId = seatIds[r];
23357
23790
  const crewRow = seatId ? crewRowById.get(seatId) : void 0;
@@ -23364,7 +23797,7 @@ ${where.join("\n")}`);
23364
23797
  name: crewRow?.name ?? null,
23365
23798
  personaName,
23366
23799
  // Additive (CREW-2822): how many runs this seat has live right now.
23367
- running: runs.filter((x) => x.role === r && x.route === routeSlug(route.route)).length
23800
+ running: runs.filter((x) => x.role === r && x.route === routeSlug(route2.route)).length
23368
23801
  };
23369
23802
  });
23370
23803
  process.stdout.write(`${JSON.stringify({
@@ -23408,30 +23841,30 @@ ${where.join("\n")}`);
23408
23841
  seats: seats2
23409
23842
  },
23410
23843
  // Additive (CREW-2822): every live run on this ship. `version` stays 1.
23411
- runs: runs.map((x) => describeRun(x, route, seatIds)),
23844
+ runs: runs.map((x) => describeRun(x, route2, seatIds)),
23412
23845
  // Additive (CREW-2857): every live periscope on this ship, from its local record. `version` stays 1.
23413
23846
  periscopes,
23414
23847
  watermark: state.watermark(),
23415
23848
  route: {
23416
- name: route.route,
23849
+ name: route2.route,
23417
23850
  // Additive (CREW-2248): the running version and the workspace's floor. `version` stays 1.
23418
23851
  crewVersion: sentVersion(cfg.ship),
23419
23852
  crewMinVersion,
23420
23853
  // Additive (CREW-1496): each project and its work mode. `version` stays 1.
23421
23854
  projects: projects.map((p) => ({ id: p.id, name: p.name, workMode: p.mode })),
23422
- dir: route.dir,
23855
+ dir: route2.dir,
23423
23856
  // Additive (CREW-2548): open tickets with no repository, as of the last cycle.
23424
- unroutable: state.unroutable(route.route).previous().length,
23425
- enabled: route.enabled,
23426
- area: route.area ?? null,
23857
+ unroutable: state.unroutable(route2.route).previous().length,
23858
+ enabled: route2.enabled,
23859
+ area: route2.area ?? null,
23427
23860
  // Additive (CREW-1511): the toolchain this route's sessions run on.
23428
- toolchain: resolveToolchain(cfg.ship, route).name,
23429
- baseUrl: route.baseUrl,
23861
+ toolchain: resolveToolchain(cfg.ship, route2).name,
23862
+ baseUrl: route2.baseUrl,
23430
23863
  repos: repos.map((r) => ({
23431
23864
  name: r.name,
23432
23865
  dir: r.dir,
23433
23866
  // Additive (CREW-1529): the toolchain a ticket in this repo runs on.
23434
- toolchain: resolveToolchain(cfg.ship, route, r.name).name,
23867
+ toolchain: resolveToolchain(cfg.ship, route2, r.name).name,
23435
23868
  // Additive (CREW-1379): `unsafe` means a ship must not cut a
23436
23869
  // worktree or release from this base. `version` stays 1.
23437
23870
  baseBranch: baseHealth(r),
@@ -23453,7 +23886,7 @@ ${where.join("\n")}`);
23453
23886
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
23454
23887
  // config-provenance display; this is the field that actually
23455
23888
  // describes where a worktree lands by default.
23456
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
23889
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename13(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
23457
23890
  worktreeParent: resolvePath(r.dir, "..")
23458
23891
  })),
23459
23892
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -23482,8 +23915,8 @@ ${owed.map((l) => ` ${l}
23482
23915
  `);
23483
23916
  process.stdout.write(
23484
23917
  `ship: ${cfg.ship.name} (${cfg.ship.platform})
23485
- route: ${route.route} -> ${route.dir}
23486
- enabled: ${route.enabled}
23918
+ route: ${route2.route} -> ${route2.dir}
23919
+ enabled: ${route2.enabled}
23487
23920
  periscopes: ${periscopeSummary(cfg)}
23488
23921
  ` + periscopes.map((p) => `periscope: ${p.ticket} ${p.repo} ${p.url} ${p.state}${p.serving ? "" : " (not serving)"} expires ${p.expiresAt}
23489
23922
  `).join("") + `crew: ${state.isPaused() ? "paused" : "active"}
@@ -23501,11 +23934,11 @@ watermark: ${state.watermark()}
23501
23934
  process.stdout.write(`attention: ${i.kind} \u2014 ${i.message}
23502
23935
  `);
23503
23936
  }
23504
- for (const r of await resolvedRepos(route)) {
23937
+ for (const r of await resolvedRepos(route2)) {
23505
23938
  const bh = baseHealth(r);
23506
23939
  if (bh.unsafe) process.stdout.write(`base: ${r.name} \u2014 UNSAFE, no worktree cuts or releases: ${bh.detail}
23507
23940
  `);
23508
- process.stdout.write(`releaser: ${r.name} \u2014 ${await releaserLine(statusTracker, route.route, r.name, r.config.release)}
23941
+ process.stdout.write(`releaser: ${r.name} \u2014 ${await releaserLine(statusTracker, route2.route, r.name, r.config.release)}
23509
23942
  `);
23510
23943
  }
23511
23944
  try {
@@ -23550,14 +23983,14 @@ watermark: ${state.watermark()}
23550
23983
  {
23551
23984
  const prBased = /* @__PURE__ */ new Set();
23552
23985
  let mentionNode = false;
23553
- for (const c of [route]) {
23986
+ for (const c of [route2]) {
23554
23987
  const { settled: repos, errors } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
23555
23988
  for (const e of errors) {
23556
23989
  pushAuth.push({ route: c.route, repo: e.repo, result: { state: "failed", detail: `could not resolve this repo's configuration: ${e.detail}`, hint: "fix the repo's .crew.yaml" } });
23557
23990
  }
23558
23991
  for (const r of repos) {
23559
23992
  const remote = r.config.branch.remote;
23560
- if (existsSync38(r.dir)) {
23993
+ if (existsSync39(r.dir)) {
23561
23994
  pushAuth.push({
23562
23995
  route: c.route,
23563
23996
  repo: r.name,
@@ -23582,7 +24015,7 @@ watermark: ${state.watermark()}
23582
24015
  if (flag("json")) {
23583
24016
  const problems = [];
23584
24017
  let passed = 0;
23585
- for (const c of [route]) {
24018
+ for (const c of [route2]) {
23586
24019
  const { settled: repos, errors } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
23587
24020
  for (const e of errors) {
23588
24021
  problems.push({ severity: "blocker", kind: "repo_config_invalid", route: c.route, repo: e.repo, message: `could not resolve this repo's configuration: ${e.detail}`, remedy: "fix the repo's .crew.yaml" });
@@ -23602,19 +24035,19 @@ watermark: ${state.watermark()}
23602
24035
  }
23603
24036
  }
23604
24037
  if (!flag("offline")) {
23605
- for (const p of await probeSeatReads(route, { userAgent: cfg.ship.userAgent })) {
24038
+ for (const p of await probeSeatReads(route2, { userAgent: cfg.ship.userAgent })) {
23606
24039
  if (p.state === "forbidden") {
23607
- problems.push({ severity: "blocker", kind: "seat_cannot_read_issues", route: route.route, message: `${p.role} agent key refused on ${p.tables.filter((t) => t.state === "forbidden").map((t) => t.table).join(", ")} (403)`, remedy: describeSeatForbidden(p.role, p.tables).replace(/^.*?— /, "") });
24040
+ problems.push({ severity: "blocker", kind: "seat_cannot_read_issues", route: route2.route, message: `${p.role} agent key refused on ${p.tables.filter((t) => t.state === "forbidden").map((t) => t.table).join(", ")} (403)`, remedy: describeSeatForbidden(p.role, p.tables).replace(/^.*?— /, "") });
23608
24041
  } else if (p.state === "ok") passed++;
23609
24042
  }
23610
24043
  }
23611
24044
  for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
23612
24045
  const previewProblem2 = await previewHostProblem(cfg);
23613
24046
  if (previewProblem2) problems.push(previewHostFinding(previewProblem2));
23614
- if (cfg.routes.some((x) => x.route === route.route && x.enabled && x.hostPeriscopes)) {
23615
- const { settled } = await settleRepos(reposOf(route), (t) => t.name, (t) => resolveOneRepo(route, t));
23616
- const tablePresent = flag("offline") ? void 0 : (await new Tracker(route, cfg.ship).periscopesStatus().catch(() => ({ present: true }))).present;
23617
- problems.push(...routePeriscopeProblems({ route: route.route, hosts: true, repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview })), tablePresent }));
24047
+ if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
24048
+ const { settled } = await settleRepos(reposOf(route2), (t) => t.name, (t) => resolveOneRepo(route2, t));
24049
+ const tablePresent = flag("offline") ? void 0 : (await new Tracker(route2, cfg.ship).periscopesStatus().catch(() => ({ present: true }))).present;
24050
+ problems.push(...routePeriscopeProblems({ route: route2.route, hosts: true, repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview })), tablePresent }));
23618
24051
  }
23619
24052
  problems.push(...attentionProblems(openShipAttention(state), problems));
23620
24053
  if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
@@ -23640,7 +24073,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23640
24073
  }
23641
24074
  try {
23642
24075
  const evFile = eventFileFor(cfg.ship.stateDir);
23643
- const evSize = existsSync38(evFile) ? statSync12(evFile).size : 0;
24076
+ const evSize = existsSync39(evFile) ? statSync13(evFile).size : 0;
23644
24077
  process.stdout.write(
23645
24078
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
23646
24079
  ` : "events log: ok\n"
@@ -23667,15 +24100,15 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23667
24100
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
23668
24101
  if (changed2) {
23669
24102
  process.stdout.write(
23670
- `daemon config: WARN ${changed2} changed at ${new Date(statSync12(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
24103
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync13(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
23671
24104
  `
23672
24105
  );
23673
24106
  }
23674
24107
  }
23675
- const tracker = new Tracker(route, cfg.ship);
24108
+ const tracker = new Tracker(route2, cfg.ship);
23676
24109
  {
23677
24110
  const have = sentVersion(cfg.ship);
23678
- const floor = await readCrewFloor(route, cfg.ship);
24111
+ const floor = await readCrewFloor(route2, cfg.ship);
23679
24112
  if (have && floor) {
23680
24113
  process.stdout.write(
23681
24114
  `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`)" : ""}
@@ -23692,9 +24125,9 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23692
24125
  ` + contractProblems.map((p) => ` - ${p}
23693
24126
  `).join("") : "contract: consistent\n"
23694
24127
  );
23695
- for (const r of await resolvedRepos(route)) {
24128
+ for (const r of await resolvedRepos(route2)) {
23696
24129
  const { config } = r;
23697
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
24130
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename13(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
23698
24131
  process.stdout.write(
23699
24132
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
23700
24133
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -23725,7 +24158,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23725
24158
  if (warn) process.stdout.write(` WARNING ${warn}
23726
24159
  `);
23727
24160
  }
23728
- const scope = releaseLockScope(route.route, r.name);
24161
+ const scope = releaseLockScope(route2.route, r.name);
23729
24162
  const lock2 = await tracker.lockState(scope).catch(() => void 0);
23730
24163
  if (lock2?.holder && staleOwnLockClaim(lock2.holder, cfg.ship.name)) {
23731
24164
  process.stdout.write(` ${formatStaleOwnLock(scope, lock2.holder)}`);
@@ -23742,10 +24175,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23742
24175
  `
23743
24176
  );
23744
24177
  const periscopes = await tracker.periscopesStatus();
23745
- if (cfg.routes.some((x) => x.route === route.route && x.enabled && x.hostPeriscopes)) {
23746
- const { settled } = await settleRepos(reposOf(route), (t) => t.name, (t) => resolveOneRepo(route, t));
24178
+ if (cfg.routes.some((x) => x.route === route2.route && x.enabled && x.hostPeriscopes)) {
24179
+ const { settled } = await settleRepos(reposOf(route2), (t) => t.name, (t) => resolveOneRepo(route2, t));
23747
24180
  for (const f of routePeriscopeProblems({
23748
- route: route.route,
24181
+ route: route2.route,
23749
24182
  hosts: true,
23750
24183
  tablePresent: periscopes.present,
23751
24184
  repos: settled.map((r) => ({ name: r.name, hasPreviewHook: !!r.config.hooks.preview }))
@@ -23760,26 +24193,26 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
23760
24193
  `
23761
24194
  );
23762
24195
  process.stdout.write(
23763
- route.resolved?.releaseSeat ? "release seat: this ship has its own Release Crew row\n" : "release seat: none \u2014 release comments are credited to the dev seat; run `crew connect` once the Crew table has this ship's row\n"
24196
+ route2.resolved?.releaseSeat ? "release seat: this ship has its own Release Crew row\n" : "release seat: none \u2014 release comments are credited to the dev seat; run `crew connect` once the Crew table has this ship's row\n"
23764
24197
  );
23765
- const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
23766
- const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
24198
+ const keyRole = route2.resolved?.role ?? await fetchKeyRole(route2, cfg.ship.userAgent);
24199
+ const roleRoute = route2.resolved && keyRole ? { ...route2, resolved: { ...route2.resolved, role: keyRole } } : route2;
23767
24200
  process.stdout.write(`api key role: ${keyRole ?? "unknown"}
23768
24201
  `);
23769
- for (const m of route.resolved?.capabilityMisses ?? []) process.stdout.write(`capability: MISSING ${m} (re-run \`crew connect\` once granted)
24202
+ for (const m of route2.resolved?.capabilityMisses ?? []) process.stdout.write(`capability: MISSING ${m} (re-run \`crew connect\` once granted)
23770
24203
  `);
23771
24204
  const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
23772
24205
  process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}
23773
24206
  `);
23774
24207
  for (const role of SEAT_KEY_ROLES) {
23775
- if (!route.resolved?.seats[role]) continue;
24208
+ if (!route2.resolved?.seats[role]) continue;
23776
24209
  const id = await resolveSeatKey(roleRoute, role);
23777
24210
  process.stdout.write(
23778
24211
  `seat identity: ${role} writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
23779
24212
  `
23780
24213
  );
23781
24214
  }
23782
- if (route.resolved?.releaseSeat) {
24215
+ if (route2.resolved?.releaseSeat) {
23783
24216
  const id = await resolveReleaseKey(roleRoute);
23784
24217
  process.stdout.write(
23785
24218
  `seat identity: release writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
@@ -23829,7 +24262,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
23829
24262
  }
23830
24263
  }
23831
24264
  {
23832
- const op = route.resolved?.operator;
24265
+ const op = route2.resolved?.operator;
23833
24266
  const row = op ? crewRows.find((c) => c.id === op) : void 0;
23834
24267
  if (!op) {
23835
24268
  process.stdout.write("operator identity: no operator on this route \u2014 run `crew connect`\n");
@@ -23844,7 +24277,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
23844
24277
  );
23845
24278
  process.exitCode = 1;
23846
24279
  } else {
23847
- const resolves = memberByIdentity(buildRoster(configuredMembers(route), crewRows), row.user_id);
24280
+ const resolves = memberByIdentity(buildRoster(configuredMembers(route2), crewRows), row.user_id);
23848
24281
  process.stdout.write(
23849
24282
  resolves ? `operator identity: comments from ${row.name ?? op} resolve to ${crewLabel(resolves)}
23850
24283
  ` : `operator identity: ERROR user_id ${row.user_id} does not resolve through the roster
@@ -23855,7 +24288,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
23855
24288
  }
23856
24289
  if (ships.length > 0) {
23857
24290
  try {
23858
- const humanHolds = new Set(holdIds(buildRoster(configuredMembers(route), crewRows)));
24291
+ const humanHolds = new Set(holdIds(buildRoster(configuredMembers(route2), crewRows)));
23859
24292
  const warnings = holdWarnings(await tracker.openTickets(), ships, tracker.contract, humanHolds);
23860
24293
  process.stdout.write(
23861
24294
  warnings.length === 0 ? "ship holds: every in-flight ticket is held by a live ship\n" : warnings.map((w) => `ship holds: WARNING ${w}
@@ -23910,7 +24343,7 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
23910
24343
  `);
23911
24344
  }
23912
24345
  }
23913
- for (const [repoName, ov] of Object.entries(route.repoOverrides)) {
24346
+ for (const [repoName, ov] of Object.entries(route2.repoOverrides)) {
23914
24347
  if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
23915
24348
  `);
23916
24349
  }
@@ -23924,15 +24357,15 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
23924
24357
  `
23925
24358
  );
23926
24359
  }
23927
- if (route.apiKey) {
23928
- const mcpUrl = mcpUrlFor(route.baseUrl);
24360
+ if (route2.apiKey) {
24361
+ const mcpUrl = mcpUrlFor(route2.baseUrl);
23929
24362
  const seenMcp = /* @__PURE__ */ new Set();
23930
24363
  for (const tc of shipToolchains(cfg.ship)) {
23931
24364
  const id = `${tc.harness}\0${tc.bin}`;
23932
24365
  if (seenMcp.has(id)) continue;
23933
24366
  seenMcp.add(id);
23934
24367
  const adapter = adapterFor(tc.harness);
23935
- const m = adapter.inspectMcp({ bin: tc.bin, url: mcpUrl, key: route.apiKey });
24368
+ const m = adapter.inspectMcp({ bin: tc.bin, url: mcpUrl, key: route2.apiKey });
23936
24369
  process.stdout.write(
23937
24370
  m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
23938
24371
  ` : m.state === "no-binary" ? `mcp: ${tc.bin} not found \u2014 ${adapter.displayName} was not checked
@@ -23944,22 +24377,22 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
23944
24377
  }
23945
24378
  for (const h of shipPair(cfg.ship).filter((x) => x !== "claude")) {
23946
24379
  const inst = pairInstallerFor(h);
23947
- const v = inst.verifyPair({ url: mcpUrlFor(route.baseUrl) });
24380
+ const v = inst.verifyPair({ url: mcpUrlFor(route2.baseUrl) });
23948
24381
  process.stdout.write(`pair ${h}:`.padEnd(19) + (v.ok ? ` installed (${v.detail})
23949
24382
  ` : ` NOT installed \u2014 ${v.detail}; run crew connect
23950
24383
  `));
23951
- const envWarn = inst.envWarning?.({ url: mcpUrlFor(route.baseUrl) });
24384
+ const envWarn = inst.envWarning?.({ url: mcpUrlFor(route2.baseUrl) });
23952
24385
  if (envWarn) process.stdout.write(`pair ${h} env:`.padEnd(19) + ` WARN \u2014 ${envWarn}
23953
24386
  `);
23954
24387
  }
23955
24388
  const shipVendors = new Set(shipToolchains(cfg.ship).map(toolchainVendor));
23956
- if (route.apiKey && route.resolved) {
24389
+ if (route2.apiKey && route2.resolved) {
23957
24390
  let flagged = 0;
23958
24391
  for (const role of ROLE_NAMES) {
23959
- const persona = await fetchSeatPersona(route, role, { userAgent: cfg.ship.userAgent });
24392
+ const persona = await fetchSeatPersona(route2, role, { userAgent: cfg.ship.userAgent });
23960
24393
  if (persona?.vendor && !shipVendors.has(persona.vendor)) {
23961
24394
  flagged++;
23962
- process.stdout.write(`seat vendor: ${role} persona wants "${persona.vendor}" \u2014 no toolchain on this ship serves it (have: ${[...shipVendors].join(", ")}); it will run ${resolveSeat(cfg.ship, route, role).toolchain.name} with a warning
24395
+ process.stdout.write(`seat vendor: ${role} persona wants "${persona.vendor}" \u2014 no toolchain on this ship serves it (have: ${[...shipVendors].join(", ")}); it will run ${resolveSeat(cfg.ship, route2, role).toolchain.name} with a warning
23963
24396
  `);
23964
24397
  }
23965
24398
  }