@tablation/crew 0.50.1 → 0.50.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +259 -127
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,5 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/doctor-repo.ts
4
+ import { existsSync } from "fs";
5
+ function doctorBaseReport(r, base, health) {
6
+ const pad = " ";
7
+ if (!existsSync(r.dir)) {
8
+ return {
9
+ text: `${pad}- ${r.dir} is missing on disk \u2014 no .crew.yaml read, settings above are defaults
10
+ `,
11
+ failed: true,
12
+ skipRest: true
13
+ };
14
+ }
15
+ try {
16
+ const bh = health(r);
17
+ if (bh.action === "not-applicable") return { text: "", failed: false, skipRest: false };
18
+ return {
19
+ text: `${pad}base ${base}: ${bh.unsafe ? "ERROR \u2014 " : ""}${bh.detail}
20
+ `,
21
+ failed: bh.unsafe,
22
+ skipRest: false
23
+ };
24
+ } catch (e) {
25
+ return {
26
+ text: `${pad}base ${base}: could not check \u2014 ${e instanceof Error ? e.message : String(e)}
27
+ `,
28
+ failed: true,
29
+ skipRest: false
30
+ };
31
+ }
32
+ }
33
+
3
34
  // src/cli.ts
4
35
  import { resolve as resolve4, dirname as dirname12, join as join25, basename as basename9 } from "path";
5
36
  import { fileURLToPath } from "url";
@@ -62,7 +93,7 @@ function pickWorktreeRoute(ticketNumber, candidates) {
62
93
  }
63
94
 
64
95
  // src/config.ts
65
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
96
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
66
97
  import { resolve, dirname as dirname3, isAbsolute, join as join7, basename as basename3 } from "path";
67
98
  import { homedir as homedir5, tmpdir } from "os";
68
99
  import { parse as parse2 } from "yaml";
@@ -71,7 +102,7 @@ import { parse as parse2 } from "yaml";
71
102
  import {
72
103
  appendFileSync,
73
104
  mkdirSync,
74
- existsSync,
105
+ existsSync as existsSync2,
75
106
  statSync,
76
107
  renameSync,
77
108
  openSync,
@@ -90,7 +121,7 @@ var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
90
121
  var ROTATE_LOCK_STALE_MS = 6e4;
91
122
  function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
92
123
  try {
93
- if (!existsSync(file) || statSync(file).size <= maxBytes) return void 0;
124
+ if (!existsSync2(file) || statSync(file).size <= maxBytes) return void 0;
94
125
  } catch {
95
126
  return void 0;
96
127
  }
@@ -111,7 +142,7 @@ function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
111
142
  if (statSync(file).size <= maxBytes) return void 0;
112
143
  const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
113
144
  let dest = join(dirname(file), `events.${stamp}.jsonl`);
114
- for (let n = 1; existsSync(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
145
+ for (let n = 1; existsSync2(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
115
146
  renameSync(file, dest);
116
147
  return dest;
117
148
  } finally {
@@ -213,7 +244,7 @@ var Emitter = class _Emitter {
213
244
  var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
214
245
 
215
246
  // src/pair-install.ts
216
- import { existsSync as existsSync4, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
247
+ import { existsSync as existsSync5, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
217
248
  import { spawnSync as spawnSync3 } from "child_process";
218
249
  import { homedir as homedir4 } from "os";
219
250
  import { join as join5 } from "path";
@@ -342,7 +373,7 @@ function removeClaudeMcp(o) {
342
373
 
343
374
  // src/launcher.ts
344
375
  import { basename, join as join3 } from "path";
345
- import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
376
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
346
377
  import { homedir as homedir2 } from "os";
347
378
  function isOriLauncher(spec) {
348
379
  return !!spec.launcher && basename(spec.launcher) === "ori";
@@ -405,7 +436,7 @@ function omlxModelPresent(model, dirs) {
405
436
  }
406
437
  function readOmlxSettings(home = homedir2()) {
407
438
  const p = join3(home, ".omlx", "settings.json");
408
- return existsSync2(p) ? parseOmlxSettings(readFileSync3(p, "utf8")) : void 0;
439
+ return existsSync3(p) ? parseOmlxSettings(readFileSync3(p, "utf8")) : void 0;
409
440
  }
410
441
 
411
442
  // src/capability.ts
@@ -542,7 +573,7 @@ import { join as join4 } from "path";
542
573
  import { rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
543
574
 
544
575
  // src/pair-files.ts
545
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync, rmSync as rmSync2 } from "fs";
576
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync, rmSync as rmSync2 } from "fs";
546
577
  import { dirname as dirname2 } from "path";
547
578
  var PAIR_BLOCK_START = "<!-- crew:pair start -->";
548
579
  var PAIR_BLOCK_END = "<!-- crew:pair end -->";
@@ -583,7 +614,7 @@ function removePairBlock(text) {
583
614
  return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
584
615
  }
585
616
  function readIfExists(path) {
586
- return existsSync3(path) ? readFileSync4(path, "utf8") : void 0;
617
+ return existsSync4(path) ? readFileSync4(path, "utf8") : void 0;
587
618
  }
588
619
  function writeFileMkdir(path, text) {
589
620
  mkdirSync2(dirname2(path), { recursive: true });
@@ -980,7 +1011,7 @@ function vscodeUserDirs(o) {
980
1011
  const platform2 = o.platform ?? process.platform;
981
1012
  const root = platform2 === "darwin" ? join5(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join5(home, "AppData", "Roaming") : join5(home, ".config");
982
1013
  const all = VSCODE_FLAVOURS.map((f) => join5(root, f, "User"));
983
- const present = all.filter((d) => existsSync4(d));
1014
+ const present = all.filter((d) => existsSync5(d));
984
1015
  return present.length > 0 ? present : [all[0]];
985
1016
  }
986
1017
  var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
@@ -1372,7 +1403,7 @@ function explain(ship, required) {
1372
1403
  }
1373
1404
 
1374
1405
  // src/repo-config.ts
1375
- import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
1406
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
1376
1407
  import { join as join6, basename as basename2 } from "path";
1377
1408
  import { parse } from "yaml";
1378
1409
  var SPEC_VERSION = 1;
@@ -1456,7 +1487,7 @@ var PROVIDERS = ["github", "buildkite", "other", "none"];
1456
1487
  function findRepoConfig(dir) {
1457
1488
  for (const name of REPO_CONFIG_FILENAMES) {
1458
1489
  const p = join6(dir, name);
1459
- if (existsSync5(p)) return p;
1490
+ if (existsSync6(p)) return p;
1460
1491
  }
1461
1492
  return null;
1462
1493
  }
@@ -2181,7 +2212,7 @@ function configSearchPath(crewHome) {
2181
2212
  }
2182
2213
  function findConfigFile(crewHome) {
2183
2214
  const candidates = configSearchPath(crewHome);
2184
- return candidates.find((c) => existsSync6(c)) ?? candidates[candidates.length - 1];
2215
+ return candidates.find((c) => existsSync7(c)) ?? candidates[candidates.length - 1];
2185
2216
  }
2186
2217
  var defaultConfigPath = () => join7(process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config"), "crew", "crew.yaml");
2187
2218
  function routeSlug(route) {
@@ -2204,7 +2235,7 @@ function splitRoute(route) {
2204
2235
  }
2205
2236
  function readResolvedFile(stateDir, route) {
2206
2237
  const path = resolvedPathFor(stateDir, route);
2207
- if (!existsSync6(path)) return void 0;
2238
+ if (!existsSync7(path)) return void 0;
2208
2239
  try {
2209
2240
  return JSON.parse(readFileSync6(path, "utf8"));
2210
2241
  } catch (e) {
@@ -2257,13 +2288,14 @@ function parseResolved(raw, m, where) {
2257
2288
  contract: raw.contract ?? void 0,
2258
2289
  ...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
2259
2290
  ...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
2291
+ ...typeof raw.role === "string" && raw.role ? { role: raw.role } : {},
2260
2292
  ...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
2261
2293
  ...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
2262
2294
  };
2263
2295
  }
2264
2296
  function loadConfig(crewHome, configFile, opts = {}) {
2265
2297
  const file = configFile ?? findConfigFile(crewHome);
2266
- if (opts.text === void 0 && !existsSync6(file)) {
2298
+ if (opts.text === void 0 && !existsSync7(file)) {
2267
2299
  throw new ConfigError(
2268
2300
  `no config found. Looked in:
2269
2301
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
@@ -2573,7 +2605,7 @@ function repoIdForName(r, name) {
2573
2605
  }
2574
2606
  function resolveApiKey(r) {
2575
2607
  if (r.apiKey) return r.apiKey;
2576
- if (r.apiKeyFile && r.apiKeyVar && existsSync6(r.apiKeyFile)) {
2608
+ if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
2577
2609
  for (const line of readFileSync6(r.apiKeyFile, "utf8").split("\n")) {
2578
2610
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
2579
2611
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
@@ -2793,7 +2825,7 @@ function rosterMarkdown(roster, meId) {
2793
2825
  }
2794
2826
 
2795
2827
  // src/state.ts
2796
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
2828
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
2797
2829
  import { join as join8 } from "path";
2798
2830
  var EPOCH = "1970-01-01T00:00:00.000Z";
2799
2831
  var State = class _State {
@@ -2839,10 +2871,10 @@ var State = class _State {
2839
2871
  }
2840
2872
  /** Whole-crew pause, and the per-role sentinels one level down. */
2841
2873
  isPaused() {
2842
- return existsSync7(this.path(".crew-paused"));
2874
+ return existsSync8(this.path(".crew-paused"));
2843
2875
  }
2844
2876
  isRolePaused(role) {
2845
- return existsSync7(this.path(`.role-paused-${role}`));
2877
+ return existsSync8(this.path(`.role-paused-${role}`));
2846
2878
  }
2847
2879
  pausedRoles(roles) {
2848
2880
  return new Set(roles.filter((r) => this.isRolePaused(r)));
@@ -2859,7 +2891,7 @@ var State = class _State {
2859
2891
  * while agents keep working.
2860
2892
  */
2861
2893
  isReleasePaused() {
2862
- return existsSync7(this.path(".release-paused"));
2894
+ return existsSync8(this.path(".release-paused"));
2863
2895
  }
2864
2896
  setReleasePaused(paused) {
2865
2897
  if (paused) writeFileSync4(this.path(".release-paused"), "");
@@ -3507,7 +3539,7 @@ function describeRelease(o, route) {
3507
3539
  // src/install.ts
3508
3540
  import { dirname as dirname4, join as join9, extname } from "path";
3509
3541
  import { homedir as homedir6 } from "os";
3510
- import { accessSync, constants, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
3542
+ import { accessSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
3511
3543
  import { spawnSync as spawnSync4 } from "child_process";
3512
3544
  import { createHash } from "crypto";
3513
3545
 
@@ -3882,7 +3914,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3882
3914
  }
3883
3915
  }
3884
3916
  for (const path of plan.unitPaths) {
3885
- if (!existsSync8(path)) continue;
3917
+ if (!existsSync9(path)) continue;
3886
3918
  if (dryRun) {
3887
3919
  log.emit(`would remove ${path}`);
3888
3920
  continue;
@@ -3918,7 +3950,7 @@ function loadedCrewLabels() {
3918
3950
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3919
3951
  }
3920
3952
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3921
- const files = existsSync8(dir) ? readdirSync2(dir) : [];
3953
+ const files = existsSync9(dir) ? readdirSync2(dir) : [];
3922
3954
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3923
3955
  const plistPath = join9(dir, f);
3924
3956
  let program = null;
@@ -3930,7 +3962,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3930
3962
  label: f.slice(0, -".plist".length),
3931
3963
  plistPath,
3932
3964
  program,
3933
- programExists: program !== null && existsSync8(program)
3965
+ programExists: program !== null && existsSync9(program)
3934
3966
  };
3935
3967
  });
3936
3968
  const seen = new Set(units.map((u) => u.label));
@@ -3949,9 +3981,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3949
3981
  log.emit(`would unload and remove ${unit.label}`);
3950
3982
  return;
3951
3983
  }
3952
- if (existsSync8(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3984
+ if (existsSync9(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3953
3985
  spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
3954
- if (existsSync8(unit.plistPath)) unlinkSync(unit.plistPath);
3986
+ if (existsSync9(unit.plistPath)) unlinkSync(unit.plistPath);
3955
3987
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3956
3988
  }
3957
3989
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -4034,7 +4066,7 @@ import { statfsSync } from "fs";
4034
4066
  // src/git.ts
4035
4067
  import { execFileSync } from "child_process";
4036
4068
  import {
4037
- existsSync as existsSync9,
4069
+ existsSync as existsSync10,
4038
4070
  mkdirSync as mkdirSync5,
4039
4071
  openSync as openSync2,
4040
4072
  closeSync as closeSync2,
@@ -4434,7 +4466,7 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
4434
4466
  try {
4435
4467
  const st = statSync4(root);
4436
4468
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
4437
- if (existsSync9(join10(root, "checkout"))) continue;
4469
+ if (existsSync10(join10(root, "checkout"))) continue;
4438
4470
  rmSync6(root, { recursive: true, force: true });
4439
4471
  removed.push(name);
4440
4472
  } catch {
@@ -4477,7 +4509,7 @@ function originUrlOf(dir, remoteName) {
4477
4509
  }
4478
4510
  }
4479
4511
  function ensureRepoCheckout(dir, remote, opts = {}) {
4480
- if (existsSync9(`${dir}/.git`)) return false;
4512
+ if (existsSync10(`${dir}/.git`)) return false;
4481
4513
  if (!remote) {
4482
4514
  throw new GitError(
4483
4515
  `${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
@@ -4540,10 +4572,10 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4540
4572
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
4541
4573
  const release = acquireCheckoutLock(dir);
4542
4574
  try {
4543
- if (!existsSync9(`${dir}/.git`) && githubRemote) {
4575
+ if (!existsSync10(`${dir}/.git`) && githubRemote) {
4544
4576
  const url = cloneUrlFor(githubRemote, clone);
4545
4577
  const marker = cloneFailureMarker(dir);
4546
- if (existsSync9(marker) && readFileSync9(marker, "utf8").split("\n")[0] === url) {
4578
+ if (existsSync10(marker) && readFileSync9(marker, "utf8").split("\n")[0] === url) {
4547
4579
  return { action: "clone-failed", detail: readFileSync9(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4548
4580
  }
4549
4581
  try {
@@ -7126,7 +7158,7 @@ var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id ===
7126
7158
 
7127
7159
  // src/agent.ts
7128
7160
  import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
7129
- import { readFileSync as readFileSync12, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
7161
+ import { readFileSync as readFileSync12, existsSync as existsSync13, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
7130
7162
  import { StringDecoder } from "string_decoder";
7131
7163
  import { finished } from "stream/promises";
7132
7164
  import { join as join14, dirname as dirname6 } from "path";
@@ -7544,7 +7576,7 @@ function expandSkillMarkers(text, lookup, opts = {}) {
7544
7576
  }
7545
7577
 
7546
7578
  // src/skills.ts
7547
- import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync11 } from "fs";
7579
+ import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync12 } from "fs";
7548
7580
  import { join as join13 } from "path";
7549
7581
  import { parse as parseYaml } from "yaml";
7550
7582
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError8, TablationApiError as TablationApiError2 } from "@tablation/client";
@@ -7561,7 +7593,7 @@ ${additions}`;
7561
7593
  }
7562
7594
 
7563
7595
  // src/agents.ts
7564
- import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
7596
+ import { readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
7565
7597
  import { join as join12 } from "path";
7566
7598
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError7, TablationApiError } from "@tablation/client";
7567
7599
  var PERSONA_NAME = {
@@ -7577,10 +7609,10 @@ var AgentsSyncError = class extends Error {
7577
7609
  };
7578
7610
  function personaDefaultPrompt(promptsDir, role) {
7579
7611
  const briefPath = join12(promptsDir, "personas", `lane-${role}.md`);
7580
- if (!existsSync10(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7612
+ if (!existsSync11(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7581
7613
  if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
7582
7614
  const commonPath = join12(promptsDir, "personas", "common.md");
7583
- if (!existsSync10(commonPath)) return readFileSync10(briefPath, "utf8");
7615
+ if (!existsSync11(commonPath)) return readFileSync10(briefPath, "utf8");
7584
7616
  return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
7585
7617
  }
7586
7618
  async function resolveAgentId(route, role, opts) {
@@ -7693,6 +7725,25 @@ async function fetchLatestHistoryId(route, agentsModelId, recordId, userAgent) {
7693
7725
  return void 0;
7694
7726
  }
7695
7727
  }
7728
+ function isAdminRole(role) {
7729
+ return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
7730
+ }
7731
+ async function fetchKeyRole(route, userAgent) {
7732
+ if (!route.resolved) return void 0;
7733
+ try {
7734
+ const res = await fetch(`${route.baseUrl.replace(/\/+$/, "")}/api/auth/me?workspaceId=${route.resolved.workspaceId}`, {
7735
+ headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": userAgent ?? "Mozilla/5.0 TablationCrewAgent/1.0" }
7736
+ });
7737
+ if (!res.ok) return void 0;
7738
+ const me = await res.json();
7739
+ return typeof me.role === "string" ? me.role : void 0;
7740
+ } catch {
7741
+ return void 0;
7742
+ }
7743
+ }
7744
+ function isForbidden(e) {
7745
+ return e instanceof TablationApiError && e.status === 403;
7746
+ }
7696
7747
  function promptSourceFields(model, prompt) {
7697
7748
  return model.fields?.some((f) => f.columnName === "prompt_baseline") ? { prompt_source: "crew", prompt_baseline: prompt } : {};
7698
7749
  }
@@ -7736,6 +7787,16 @@ async function syncPersonas(route, opts) {
7736
7787
  const name = PERSONA_NAME[role];
7737
7788
  const defaultPrompt = personaDefaultPrompt(route.promptsDir, role);
7738
7789
  const row = rows.find((r) => r.name === name);
7790
+ if (opts.readOnly) {
7791
+ if (!row) {
7792
+ outcomes.push({ role, action: "missing", agentId: "" });
7793
+ continue;
7794
+ }
7795
+ const prior = cache[role]?.agentId === row.id ? cache[role] : void 0;
7796
+ agentPersonas[role] = { agentId: row.id, lastSyncedUpdatedAt: prior?.lastSyncedUpdatedAt ?? row.updated_at, ...prior?.historyId ? { historyId: prior.historyId } : {} };
7797
+ outcomes.push({ role, action: "adopted", agentId: row.id });
7798
+ continue;
7799
+ }
7739
7800
  if (!row) {
7740
7801
  if (opts.dryRun) {
7741
7802
  outcomes.push({ role, action: "created", agentId: "(dry run)" });
@@ -7837,35 +7898,70 @@ async function syncPersonas(route, opts) {
7837
7898
  throw e;
7838
7899
  }
7839
7900
  }
7840
- const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent);
7841
- const releaseAgentId = opts.dryRun ? route.resolved.releaseAgentId : await linkReleaseSeat(route, client2, agentsModel, opts.userAgent);
7842
- return { outcomes, agentPersonas, crewLinks, ...releaseAgentId ? { releaseAgentId } : {} };
7901
+ const crewLinks = opts.dryRun ? [] : await linkCrewSeats(route, client2, agentsModel.id, agentPersonas, opts.userAgent, opts.readOnly === true);
7902
+ const release = opts.dryRun ? { agentId: route.resolved.releaseAgentId } : await linkReleaseSeat(route, client2, agentsModel, opts.userAgent, opts.readOnly === true);
7903
+ return {
7904
+ outcomes,
7905
+ agentPersonas,
7906
+ crewLinks,
7907
+ ...release.agentId ? { releaseAgentId: release.agentId } : {},
7908
+ ...release.note ? { releaseNote: release.note } : {},
7909
+ ...opts.readOnly ? { readOnly: true } : {}
7910
+ };
7843
7911
  }
7844
- async function linkReleaseSeat(route, client2, agentsModel, userAgent) {
7912
+ async function linkReleaseSeat(route, client2, agentsModel, userAgent, readOnly) {
7845
7913
  const seatId = route.resolved.releaseSeat;
7846
- if (!seatId) return void 0;
7914
+ if (!seatId) return {};
7847
7915
  const rows = await client2.records.list(agentsModel.id, { limit: 200 });
7848
- const agentId = rows.find((r) => r.name === RELEASE_AGENT_NAME)?.id ?? (await client2.records.create(agentsModel.id, {
7849
- name: RELEASE_AGENT_NAME,
7850
- prompt: RELEASE_AGENT_PROMPT,
7851
- ...promptSourceFields(agentsModel, RELEASE_AGENT_PROMPT)
7852
- })).id;
7916
+ let agentId = rows.find((r) => r.name === RELEASE_AGENT_NAME)?.id;
7917
+ if (!agentId) {
7918
+ if (readOnly) return { note: `Release: no "${RELEASE_AGENT_NAME}" Agents row in this workspace \u2014 writes as the ship until a workspace admin runs \`crew agents sync\`` };
7919
+ try {
7920
+ agentId = (await client2.records.create(agentsModel.id, {
7921
+ name: RELEASE_AGENT_NAME,
7922
+ prompt: RELEASE_AGENT_PROMPT,
7923
+ ...promptSourceFields(agentsModel, RELEASE_AGENT_PROMPT)
7924
+ })).id;
7925
+ } catch (e) {
7926
+ if (isForbidden(e)) return { note: `Release: could not create the "${RELEASE_AGENT_NAME}" Agents row (${NO_WRITE_NOTE}) \u2014 writes as the ship` };
7927
+ throw e;
7928
+ }
7929
+ }
7853
7930
  const crewModelId = route.resolved.models.crew;
7854
7931
  const crewModel = await client2.dataModels.get(crewModelId);
7855
- const columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModel.id, userAgent);
7932
+ let columnName;
7933
+ try {
7934
+ columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModel.id, userAgent);
7935
+ } catch (e) {
7936
+ if (e instanceof CrewAgentFieldMissing) return { note: `Release: ${CREW_FIELD_MISSING_NOTE}` };
7937
+ throw e;
7938
+ }
7856
7939
  const seatRow = await client2.records.get(crewModelId, seatId);
7857
7940
  if (seatRow[CREW_AGENT_COLUMN] !== agentId) await client2.records.update(crewModelId, seatId, { [columnName]: agentId });
7858
- return agentId;
7941
+ return { agentId };
7859
7942
  }
7860
- async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent) {
7943
+ var NO_WRITE_NOTE = "this key has no write access to the Agents table";
7944
+ var CREW_FIELD_MISSING_NOTE = "Crew table has no Agent field yet \u2014 a workspace admin's `crew agents sync` adds it";
7945
+ var CrewAgentFieldMissing = class extends Error {
7946
+ };
7947
+ async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userAgent, readOnly) {
7861
7948
  const seats2 = route.resolved.seats;
7862
7949
  const rolesWithSeats = ROLE_NAMES.filter((role) => seats2[role] && agentPersonas[role]);
7863
7950
  if (rolesWithSeats.length === 0) {
7951
+ if (readOnly) return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agents_row" : "no_seat" }));
7864
7952
  return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "persona_diverged" : "no_seat" }));
7865
7953
  }
7866
7954
  const crewModelId = route.resolved.models.crew;
7867
7955
  const crewModel = await client2.dataModels.get(crewModelId);
7868
- const columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent);
7956
+ let columnName;
7957
+ try {
7958
+ columnName = await ensureCrewAgentField(route, client2, crewModel, agentsModelId, userAgent);
7959
+ } catch (e) {
7960
+ if (e instanceof CrewAgentFieldMissing) {
7961
+ return ROLE_NAMES.map((role) => ({ role, action: seats2[role] ? "no_agent_field" : "no_seat" }));
7962
+ }
7963
+ throw e;
7964
+ }
7869
7965
  const outcomes = [];
7870
7966
  for (const role of ROLE_NAMES) {
7871
7967
  const seatId = seats2[role];
@@ -7875,7 +7971,7 @@ async function linkCrewSeats(route, client2, agentsModelId, agentPersonas, userA
7875
7971
  continue;
7876
7972
  }
7877
7973
  if (!agentId) {
7878
- outcomes.push({ role, action: "persona_diverged" });
7974
+ outcomes.push({ role, action: readOnly ? "no_agents_row" : "persona_diverged" });
7879
7975
  continue;
7880
7976
  }
7881
7977
  const seatRow = await client2.records.get(crewModelId, seatId);
@@ -7916,6 +8012,7 @@ async function ensureCrewAgentField(route, client2, crewModel, agentsModelId, us
7916
8012
  description: "The Agents-table row backing this seat's persona \u2014 set by `crew agents sync` (ISSUE-416), never edited by hand."
7917
8013
  })
7918
8014
  });
8015
+ if (createRes.status === 403) throw new CrewAgentFieldMissing();
7919
8016
  if (!createRes.ok) throw new AgentsSyncError(`creating the Crew table's Agent field failed: ${createRes.status} ${createRes.statusText}`);
7920
8017
  const created = await createRes.json();
7921
8018
  return created.columnName;
@@ -7933,6 +8030,10 @@ function describeSyncOutcome(o) {
7933
8030
  return `${label}: HAS LOCAL EDITS \u2014 crew's default is newer but was NOT applied; review ${o.agentId} in the app before syncing again`;
7934
8031
  case "overridden":
7935
8032
  return `${label}: override in place \u2014 prompt not synced (clear "Customize prompt" on the Agents row to resume)`;
8033
+ case "adopted":
8034
+ return `${label}: using the workspace's existing Agents row (${o.agentId})`;
8035
+ case "missing":
8036
+ return `${label}: no Agents row in this workspace \u2014 writes as the ship until a workspace admin runs \`crew agents sync\``;
7936
8037
  case "forced":
7937
8038
  return `${label}: --force overwrote local edits (previous prompt was: ${JSON.stringify(o.previousPrompt ?? "")})`;
7938
8039
  }
@@ -7946,6 +8047,10 @@ function describeCrewLink(o) {
7946
8047
  return `${label}: Crew row already references its Agents row`;
7947
8048
  case "no_seat":
7948
8049
  return `${label}: no seat configured for this role \u2014 skipped linking`;
8050
+ case "no_agent_field":
8051
+ return `${label}: ${CREW_FIELD_MISSING_NOTE}`;
8052
+ case "no_agents_row":
8053
+ return `${label}: no Agents row to link yet \u2014 a workspace admin's \`crew agents sync\` creates it; run it here afterwards to link this seat`;
7949
8054
  case "persona_diverged":
7950
8055
  return `${label}: persona has local edits and was not synced this pass \u2014 skipped linking until it's resolved`;
7951
8056
  }
@@ -7969,7 +8074,7 @@ function parseSkillFile(path) {
7969
8074
  }
7970
8075
  function listLocalSkills(promptsDir) {
7971
8076
  const dir = join13(promptsDir, "skills");
7972
- if (!existsSync11(dir)) return [];
8077
+ if (!existsSync12(dir)) return [];
7973
8078
  return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join13(dir, f)));
7974
8079
  }
7975
8080
  async function syncSkills(route, opts) {
@@ -8222,17 +8327,17 @@ var AgentSpawnError = class extends AgentError {
8222
8327
  };
8223
8328
  function planAgentRun(o) {
8224
8329
  const briefPath = join14(o.route.promptsDir, "personas", `lane-${o.role}.md`);
8225
- if (!existsSync12(briefPath)) {
8330
+ if (!existsSync13(briefPath)) {
8226
8331
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
8227
8332
  }
8228
8333
  const commonPath = join14(o.route.promptsDir, "personas", "common.md");
8229
- const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync12(commonPath);
8334
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync13(commonPath);
8230
8335
  const now = o.now ?? (() => Date.now());
8231
8336
  const routeLabel = routeSlug(o.route.route);
8232
8337
  const digestPath2 = join14(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
8233
8338
  let digest;
8234
8339
  let ageSeconds;
8235
- if (existsSync12(digestPath2)) {
8340
+ if (existsSync13(digestPath2)) {
8236
8341
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
8237
8342
  if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync12(digestPath2, "utf8");
8238
8343
  }
@@ -8735,12 +8840,12 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
8735
8840
  }
8736
8841
 
8737
8842
  // src/run-worktree.ts
8738
- import { existsSync as existsSync14 } from "fs";
8843
+ import { existsSync as existsSync15 } from "fs";
8739
8844
  import { dirname as dirname7, join as join16 } from "path";
8740
8845
 
8741
8846
  // src/ports.ts
8742
8847
  import { execFileSync as execFileSync2 } from "child_process";
8743
- import { existsSync as existsSync13 } from "fs";
8848
+ import { existsSync as existsSync14 } from "fs";
8744
8849
  import { join as join15 } from "path";
8745
8850
  var run = (cmd, args) => {
8746
8851
  try {
@@ -8786,7 +8891,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
8786
8891
  }
8787
8892
  function worktreeExists(parent, prefix, n) {
8788
8893
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
8789
- if (existsSync13(join15(parent, name, ".git"))) return true;
8894
+ if (existsSync14(join15(parent, name, ".git"))) return true;
8790
8895
  }
8791
8896
  return false;
8792
8897
  }
@@ -8862,7 +8967,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
8862
8967
  const existing = worktreeForNumber(dir, num);
8863
8968
  const located = locateBranchForTicket(dir, cfg, t, role);
8864
8969
  const baseRef = hasRemote ? `${remote}/${base}` : base;
8865
- if (existing && !existing.prunable && existsSync14(existing.path)) {
8970
+ if (existing && !existing.prunable && existsSync15(existing.path)) {
8866
8971
  const branch = existing.branch;
8867
8972
  if (status(existing.path).length > 0) {
8868
8973
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -8927,7 +9032,7 @@ function worktreeHoldsUnpushed(path, remote) {
8927
9032
  }
8928
9033
  async function finishRunWorktree(dir, cfg, branch) {
8929
9034
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
8930
- if (!wt || !existsSync14(wt.path)) return { kind: "none" };
9035
+ if (!wt || !existsSync15(wt.path)) return { kind: "none" };
8931
9036
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
8932
9037
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
8933
9038
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -8939,7 +9044,7 @@ async function finishRunWorktree(dir, cfg, branch) {
8939
9044
  if (pids.length) await killGently(pids);
8940
9045
  }
8941
9046
  gitOk(dir, ["worktree", "remove", wt.path]);
8942
- return existsSync14(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
9047
+ return existsSync15(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
8943
9048
  }
8944
9049
 
8945
9050
  // src/handoff-guard.ts
@@ -9175,12 +9280,12 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
9175
9280
 
9176
9281
  // src/connect-wizard.ts
9177
9282
  import { delimiter, dirname as dirname8, join as join17 } from "path";
9178
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
9283
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
9179
9284
  import { stringify } from "yaml";
9180
9285
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
9181
9286
  function shouldRunWizard(opts) {
9182
9287
  if (!opts.isTTY) return false;
9183
- const exists = opts.configExists ?? existsSync15;
9288
+ const exists = opts.configExists ?? existsSync16;
9184
9289
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
9185
9290
  }
9186
9291
  function firstRunConfigPath() {
@@ -9292,7 +9397,7 @@ Wrote ${o.configPath}.
9292
9397
  }
9293
9398
 
9294
9399
  // src/release-run.ts
9295
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
9400
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync17, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
9296
9401
  import { dirname as dirname9, join as join18 } from "path";
9297
9402
  import { tmpdir as tmpdir2 } from "os";
9298
9403
 
@@ -9983,7 +10088,7 @@ async function bump(o, size) {
9983
10088
  }
9984
10089
  const files = o.repo.release.versionFiles.map((f) => join18(o.cwd, f));
9985
10090
  const first = files[0];
9986
- if (!existsSync16(first)) {
10091
+ if (!existsSync17(first)) {
9987
10092
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
9988
10093
  return null;
9989
10094
  }
@@ -10019,7 +10124,7 @@ function writeChangelog(o, version, entries) {
10019
10124
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
10020
10125
  return;
10021
10126
  }
10022
- const existing = existsSync16(path) ? readFileSync13(path, "utf8") : "# Changelog\n";
10127
+ const existing = existsSync17(path) ? readFileSync13(path, "utf8") : "# Changelog\n";
10023
10128
  writeFileSync10(path, insertChangelogSection(existing, section));
10024
10129
  }
10025
10130
  async function confirm(o, expected) {
@@ -10125,7 +10230,7 @@ function openReleaseCheckout(o) {
10125
10230
  }
10126
10231
  for (const f of o.repo.worktrees.copy) {
10127
10232
  const src = join18(o.cwd, f);
10128
- if (!existsSync16(src)) continue;
10233
+ if (!existsSync17(src)) continue;
10129
10234
  mkdirSync8(dirname9(join18(dir, f)), { recursive: true });
10130
10235
  copyFileSync(src, join18(dir, f));
10131
10236
  }
@@ -10710,7 +10815,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
10710
10815
  }
10711
10816
 
10712
10817
  // src/watch.ts
10713
- import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
10818
+ import { watch as watchFile, existsSync as existsSync18, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
10714
10819
  var E = "\x1B[";
10715
10820
  var dim = (s) => `${E}2m${s}${E}0m`;
10716
10821
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -10811,7 +10916,7 @@ function renderHeaders(states, now = Date.now()) {
10811
10916
  return states.map((s) => renderHeader(s, now)).join("");
10812
10917
  }
10813
10918
  function readFrom(file, from) {
10814
- if (!existsSync17(file)) return { events: [], offset: from };
10919
+ if (!existsSync18(file)) return { events: [], offset: from };
10815
10920
  const size = statSync6(file).size;
10816
10921
  if (size < from) return readFrom(file, 0);
10817
10922
  if (size === from) return { events: [], offset: from };
@@ -11443,6 +11548,12 @@ import {
11443
11548
  // src/seat-keys.ts
11444
11549
  import { getSessionStore as getSessionStore2, hostFromUrl as hostFromUrl2 } from "@tablation/client";
11445
11550
  var SEAT_KEY_ROLES = ROLE_NAMES.filter((r) => r !== "pair");
11551
+ function keyIsAdmin(route) {
11552
+ const role = route.resolved?.role;
11553
+ return role === void 0 || role === "WORKSPACE_ADMIN" || role === "PLATFORM_ADMIN";
11554
+ }
11555
+ 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";
11556
+ var MEMBER_NO_KEY = "no agent key for this seat \u2014 minting needs a workspace admin key";
11446
11557
  function seatKeySlug(workspaceSlug, agentId) {
11447
11558
  return `${workspaceSlug}#agent:${agentId}`;
11448
11559
  }
@@ -11456,11 +11567,11 @@ function seatAgentId(route, role) {
11456
11567
  async function resolveReleaseKey(route, getStore = getSessionStore2) {
11457
11568
  const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
11458
11569
  const agentId = route.resolved?.releaseAgentId;
11459
- if (!agentId) return ship("release seat has no linked Agents row (run `crew agents sync`)");
11570
+ if (!agentId) return ship(keyIsAdmin(route) ? "release seat has no linked Agents row (run `crew agents sync`)" : `release seat ${MEMBER_NO_ROW}`);
11460
11571
  try {
11461
11572
  const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
11462
11573
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
11463
- return ship("no agent key stored for the release seat (run `crew agents sync`)");
11574
+ return ship(keyIsAdmin(route) ? "no agent key stored for the release seat (run `crew agents sync`)" : MEMBER_NO_KEY);
11464
11575
  } catch {
11465
11576
  return ship("OS keychain unavailable");
11466
11577
  }
@@ -11469,11 +11580,11 @@ async function resolveSeatKey(route, role, getStore = getSessionStore2) {
11469
11580
  const ship = (reason) => ({ kind: "ship", apiKey: resolveApiKey(route), reason });
11470
11581
  if (!SEAT_KEY_ROLES.includes(role)) return ship(`${role} always writes as the ship`);
11471
11582
  const agentId = seatAgentId(route, role);
11472
- if (!agentId) return ship("seat has no linked Agents row (run `crew agents sync`)");
11583
+ if (!agentId) return ship(keyIsAdmin(route) ? "seat has no linked Agents row (run `crew agents sync`)" : `seat ${MEMBER_NO_ROW}`);
11473
11584
  try {
11474
11585
  const session = await getStore().get(hostFromUrl2(route.baseUrl), slugFor(route, agentId));
11475
11586
  if (session?.apiKey) return { kind: "agent", apiKey: session.apiKey, agentId };
11476
- return ship("no agent key stored for this seat (run `crew agents sync`)");
11587
+ return ship(keyIsAdmin(route) ? "no agent key stored for this seat (run `crew agents sync`)" : MEMBER_NO_KEY);
11477
11588
  } catch {
11478
11589
  return ship("OS keychain unavailable");
11479
11590
  }
@@ -11830,7 +11941,7 @@ function schedulerInstalled(plan, exists, crontab) {
11830
11941
  }
11831
11942
 
11832
11943
  // src/worktree-sweep.ts
11833
- import { existsSync as existsSync18 } from "fs";
11944
+ import { existsSync as existsSync19 } from "fs";
11834
11945
  import { basename as basename8 } from "path";
11835
11946
  function planWorktreeSweep(target, tickets, contract) {
11836
11947
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
@@ -11866,7 +11977,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
11866
11977
  let removed = 0;
11867
11978
  let keptBranches = 0;
11868
11979
  for (const a of actions) {
11869
- if (!dryRun && existsSync18(a.path)) {
11980
+ if (!dryRun && existsSync19(a.path)) {
11870
11981
  let dirty = false;
11871
11982
  try {
11872
11983
  dirty = status(a.path).length > 0;
@@ -11880,7 +11991,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
11880
11991
  continue;
11881
11992
  }
11882
11993
  }
11883
- if (!dryRun && a.requirePushed && a.branch && existsSync18(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
11994
+ if (!dryRun && a.requirePushed && a.branch && existsSync19(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
11884
11995
  log.warn(
11885
11996
  `not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
11886
11997
  { ticket: a.ticket.issue_id, step: "worktree" }
@@ -12012,12 +12123,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
12012
12123
  }
12013
12124
 
12014
12125
  // src/stream-sweep.ts
12015
- import { existsSync as existsSync19, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
12126
+ import { existsSync as existsSync20, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
12016
12127
  import { join as join19 } from "path";
12017
12128
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
12018
12129
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
12019
12130
  const dir = join19(stateDir, "streams");
12020
- if (!existsSync19(dir)) return [];
12131
+ if (!existsSync20(dir)) return [];
12021
12132
  const cutoff = now - retentionDays * DAY_MS2;
12022
12133
  return readdirSync5(dir).map((name) => join19(dir, name)).filter((path) => {
12023
12134
  try {
@@ -12045,7 +12156,7 @@ function applyStreamSweep(paths, dryRun, log) {
12045
12156
  return removed;
12046
12157
  }
12047
12158
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
12048
- if (!existsSync19(stateDir)) return [];
12159
+ if (!existsSync20(stateDir)) return [];
12049
12160
  const cutoff = now - retentionDays * DAY_MS2;
12050
12161
  return readdirSync5(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join19(stateDir, name)).filter((path) => {
12051
12162
  try {
@@ -12057,13 +12168,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
12057
12168
  }
12058
12169
 
12059
12170
  // src/cli.ts
12060
- import { readFileSync as readFileSync19, existsSync as existsSync25, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
12171
+ import { readFileSync as readFileSync19, existsSync as existsSync26, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
12061
12172
  import { createInterface } from "readline/promises";
12062
12173
  import { dirname as dirOf, resolve as resolvePath } from "path";
12063
12174
 
12064
12175
  // src/ssh-keys.ts
12065
12176
  import { execFileSync as execFileSync3 } from "child_process";
12066
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
12177
+ import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
12067
12178
  import { dirname as dirname11, join as join20 } from "path";
12068
12179
  var SshKeyError = class extends Error {
12069
12180
  };
@@ -12073,7 +12184,7 @@ function shipSshKeyPathFor(stateDir) {
12073
12184
  function ensureShipSshKeypair(stateDir) {
12074
12185
  const privateKeyPath = shipSshKeyPathFor(stateDir);
12075
12186
  const publicKeyPath = `${privateKeyPath}.pub`;
12076
- if (!existsSync20(privateKeyPath)) {
12187
+ if (!existsSync21(privateKeyPath)) {
12077
12188
  mkdirSync9(dirname11(privateKeyPath), { recursive: true });
12078
12189
  try {
12079
12190
  execFileSync3("ssh-keygen", [
@@ -12094,7 +12205,7 @@ function ensureShipSshKeypair(stateDir) {
12094
12205
  }
12095
12206
  chmodSync(privateKeyPath, 384);
12096
12207
  }
12097
- if (!existsSync20(publicKeyPath)) {
12208
+ if (!existsSync21(publicKeyPath)) {
12098
12209
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
12099
12210
  }
12100
12211
  const publicKey = readFileSync14(publicKeyPath, "utf8").trim();
@@ -12313,7 +12424,7 @@ function syncAllPassengerCheckouts(cfg) {
12313
12424
 
12314
12425
  // src/tunnel.ts
12315
12426
  import { spawn as spawn3 } from "child_process";
12316
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync6 } from "fs";
12427
+ import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync6 } from "fs";
12317
12428
  import { randomInt } from "crypto";
12318
12429
  import { join as join21 } from "path";
12319
12430
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -12386,7 +12497,7 @@ function tunnelStatePath(stateDir, workspaceId) {
12386
12497
  }
12387
12498
  function readPersistedTunnel(stateDir, workspaceId) {
12388
12499
  const path = tunnelStatePath(stateDir, workspaceId);
12389
- if (!existsSync21(path)) return void 0;
12500
+ if (!existsSync22(path)) return void 0;
12390
12501
  try {
12391
12502
  return JSON.parse(readFileSync15(path, "utf8"));
12392
12503
  } catch {
@@ -12494,7 +12605,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12494
12605
  }
12495
12606
  function readAllPersistedWorkspaceIds(stateDir) {
12496
12607
  const dir = join21(stateDir, "passengers");
12497
- if (!existsSync21(dir)) return [];
12608
+ if (!existsSync22(dir)) return [];
12498
12609
  return readdirSync6(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
12499
12610
  }
12500
12611
 
@@ -12615,7 +12726,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
12615
12726
  import { createServer as createHttpServer } from "http";
12616
12727
  import { spawn as spawn4 } from "child_process";
12617
12728
  import { randomBytes } from "crypto";
12618
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
12729
+ import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
12619
12730
  import { join as join22 } from "path";
12620
12731
 
12621
12732
  // src/secret-compare.ts
@@ -12709,7 +12820,7 @@ function syncDaemonStatePath(stateDir, workspaceId) {
12709
12820
  }
12710
12821
  function readPersistedSyncDaemon(stateDir, workspaceId) {
12711
12822
  const path = syncDaemonStatePath(stateDir, workspaceId);
12712
- if (!existsSync22(path)) return void 0;
12823
+ if (!existsSync23(path)) return void 0;
12713
12824
  try {
12714
12825
  return JSON.parse(readFileSync16(path, "utf8"));
12715
12826
  } catch {
@@ -12740,7 +12851,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
12740
12851
  }
12741
12852
  function readAllPersistedSyncWorkspaceIds(stateDir) {
12742
12853
  const dir = join22(stateDir, "passengers");
12743
- if (!existsSync22(dir)) return [];
12854
+ if (!existsSync23(dir)) return [];
12744
12855
  const suffix = "-sync.json";
12745
12856
  return readdirSync7(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
12746
12857
  }
@@ -12797,11 +12908,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
12797
12908
  }
12798
12909
 
12799
12910
  // src/daemon.ts
12800
- import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12911
+ import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12801
12912
  import { join as join24 } from "path";
12802
12913
 
12803
12914
  // src/interrupted.ts
12804
- import { existsSync as existsSync23, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
12915
+ import { existsSync as existsSync24, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
12805
12916
  import { join as join23 } from "path";
12806
12917
  var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
12807
12918
  function interruptedComment(b) {
@@ -12820,7 +12931,7 @@ var pidAlive = (pid) => {
12820
12931
  };
12821
12932
  function runFinished(streamPath) {
12822
12933
  const path = `${streamPath}.meta.json`;
12823
- if (!existsSync23(path)) return false;
12934
+ if (!existsSync24(path)) return false;
12824
12935
  try {
12825
12936
  return JSON.parse(readFileSync17(path, "utf8")).running !== true;
12826
12937
  } catch {
@@ -12829,7 +12940,7 @@ function runFinished(streamPath) {
12829
12940
  }
12830
12941
  function orphanedStreams(o) {
12831
12942
  const dir = join23(o.stateDir, "streams");
12832
- if (!existsSync23(dir)) return [];
12943
+ if (!existsSync24(dir)) return [];
12833
12944
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
12834
12945
  const out = [];
12835
12946
  for (const name of readdirSync8(dir)) {
@@ -12943,7 +13054,7 @@ function computeTreeSignature(paths) {
12943
13054
  }
12944
13055
  function makeStaleChecker(paths, opts = {}) {
12945
13056
  const initial = computeTreeSignature(paths);
12946
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync24(opts.reloadSentinel);
13057
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync25(opts.reloadSentinel);
12947
13058
  }
12948
13059
  function reloadSentinelFor(stateDir) {
12949
13060
  return join24(stateDir, "reload-requested");
@@ -13829,7 +13940,7 @@ if (command === "passenger-sync-daemon") {
13829
13940
  if (host === "linux" && hasSystemd) {
13830
13941
  const label = labelFor(CREW_HOME, "run");
13831
13942
  const staleTimer = join25(homedir8(), ".config", "systemd", "user", `${label}.timer`);
13832
- if (existsSync25(staleTimer)) {
13943
+ if (existsSync26(staleTimer)) {
13833
13944
  process.stdout.write(
13834
13945
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
13835
13946
  `
@@ -13855,7 +13966,7 @@ if (command === "passenger-sync-daemon") {
13855
13966
  const tracker = new Tracker(c, cfg.ship);
13856
13967
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
13857
13968
  for (const r of await resolvedRepos(c)) {
13858
- if (!existsSync25(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
13969
+ if (!existsSync26(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
13859
13970
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
13860
13971
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
13861
13972
  }
@@ -13873,7 +13984,7 @@ if (command === "passenger-sync-daemon") {
13873
13984
  }
13874
13985
  async function doctorFix(o) {
13875
13986
  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))));
13876
- const unitExists = (p) => existsSync25(p);
13987
+ const unitExists = (p) => existsSync26(p);
13877
13988
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
13878
13989
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
13879
13990
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -14993,9 +15104,18 @@ ${changed2.map((c) => ` - ${c}
14993
15104
  `);
14994
15105
  process.exit(2);
14995
15106
  }
15107
+ const keyRole = target.resolved.role ?? await fetchKeyRole(target, cfg.ship.userAgent);
15108
+ let readOnly = !isAdminRole(keyRole);
14996
15109
  let personaResult;
15110
+ const runPersonas = (ro) => syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force, readOnly: ro });
14997
15111
  try {
14998
- personaResult = await syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force });
15112
+ try {
15113
+ personaResult = await runPersonas(readOnly);
15114
+ } catch (e) {
15115
+ if (readOnly || !isForbidden(e)) throw e;
15116
+ readOnly = true;
15117
+ personaResult = await runPersonas(true);
15118
+ }
14999
15119
  } catch (e) {
15000
15120
  if (e instanceof AgentsSyncError) {
15001
15121
  process.stderr.write(`crew agents sync: ${e.message}
@@ -15009,28 +15129,32 @@ ${changed2.map((c) => ` - ${c}
15009
15129
  for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
15010
15130
  `);
15011
15131
  for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
15132
+ `);
15133
+ if (personaResult.releaseNote) process.stdout.write(`${personaResult.releaseNote}
15012
15134
  `);
15013
15135
  const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
15014
15136
  for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
15015
15137
  `);
15016
15138
  let skillResult;
15017
- try {
15018
- skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
15019
- } catch (e) {
15020
- if (e instanceof SkillSyncError) {
15021
- process.stderr.write(`crew agents sync: ${e.message}
15139
+ if (!readOnly) {
15140
+ try {
15141
+ skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
15142
+ } catch (e) {
15143
+ if (isForbidden(e)) readOnly = true;
15144
+ else if (e instanceof SkillSyncError) {
15145
+ process.stderr.write(`crew agents sync: ${e.message}
15022
15146
  `);
15023
- process.exit(2);
15147
+ process.exit(2);
15148
+ } else throw e;
15024
15149
  }
15025
- throw e;
15026
15150
  }
15027
- for (const o of skillResult.outcomes) process.stdout.write(`${describeSkillSyncOutcome(o)}
15151
+ for (const o of skillResult?.outcomes ?? []) process.stdout.write(`${describeSkillSyncOutcome(o)}
15028
15152
  `);
15029
15153
  if (!dryRun) {
15030
15154
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
15031
- const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15155
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15032
15156
  raw.agentPersonas = personaResult.agentPersonas;
15033
- raw.agentSkills = skillResult.agentSkills;
15157
+ if (skillResult) raw.agentSkills = skillResult.agentSkills;
15034
15158
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
15035
15159
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
15036
15160
  writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -15040,11 +15164,16 @@ ${changed2.map((c) => ` - ${c}
15040
15164
  target.resolved.agentPersonas = personaResult.agentPersonas;
15041
15165
  if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
15042
15166
  }
15043
- for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
15044
- process.stdout.write(`${describeMintOutcome(o)}
15167
+ if (readOnly) {
15168
+ process.stdout.write("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)\n");
15169
+ process.stdout.write("seat keys: not minted (needs a workspace admin key)\n");
15170
+ } else {
15171
+ for (const o of await mintMissingSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun, shipName: cfg.ship.name })) {
15172
+ process.stdout.write(`${describeMintOutcome(o)}
15045
15173
  `);
15174
+ }
15046
15175
  }
15047
- if (personaResult.outcomes.some((o) => o.action === "diverged") || skillResult.outcomes.some((o) => o.action === "diverged")) {
15176
+ if (personaResult.outcomes.some((o) => o.action === "diverged") || (skillResult?.outcomes.some((o) => o.action === "diverged") ?? false)) {
15048
15177
  process.exitCode = 1;
15049
15178
  }
15050
15179
  break;
@@ -15088,7 +15217,7 @@ ${changed2.map((c) => ` - ${c}
15088
15217
  `);
15089
15218
  if (!dryRun) {
15090
15219
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
15091
- const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15220
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15092
15221
  raw.agentSkills = result.agentSkills;
15093
15222
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
15094
15223
  writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -15111,7 +15240,7 @@ ${changed2.map((c) => ` - ${c}
15111
15240
  if (sub === "list" && !routeName) usage2();
15112
15241
  if (sub === "add") {
15113
15242
  try {
15114
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync25(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
15243
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync26(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
15115
15244
  routeName = a.route;
15116
15245
  addPath = a.path;
15117
15246
  addName = a.name;
@@ -15126,7 +15255,7 @@ ${changed2.map((c) => ` - ${c}
15126
15255
  }
15127
15256
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
15128
15257
  let resolvedRemotes;
15129
- if (existsSync25(resolvedFile)) {
15258
+ if (existsSync26(resolvedFile)) {
15130
15259
  try {
15131
15260
  resolvedRemotes = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15132
15261
  } catch {
@@ -15143,14 +15272,14 @@ ${changed2.map((c) => ` - ${c}
15143
15272
  for (const r of repos) {
15144
15273
  const origin2 = originOf(r.dir);
15145
15274
  const row = matchRepoName(origin2, resolvedRemotes);
15146
- const note = !existsSync25(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
15275
+ const note = !existsSync26(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
15147
15276
  process.stdout.write(`${r.name} ${r.dir} ${note}
15148
15277
  `);
15149
15278
  }
15150
15279
  break;
15151
15280
  }
15152
15281
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
15153
- const top = existsSync25(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
15282
+ const top = existsSync26(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
15154
15283
  if (!top) throw new ReposError(`${target} is not a git checkout`);
15155
15284
  const origin = originOf(top);
15156
15285
  const plan = planRepoAdd({
@@ -15199,7 +15328,7 @@ ${changed2.map((c) => ` - ${c}
15199
15328
  const created = await rowTracker.createRepoRow(rowPayload);
15200
15329
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
15201
15330
  `);
15202
- if (existsSync25(resolvedFile)) {
15331
+ if (existsSync26(resolvedFile)) {
15203
15332
  const raw = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15204
15333
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
15205
15334
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
@@ -15446,7 +15575,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15446
15575
  operator = await pickOperator(found.holds);
15447
15576
  }
15448
15577
  let previous;
15449
- const hadPreviousFile = existsSync25(resolvedPath);
15578
+ const hadPreviousFile = existsSync26(resolvedPath);
15450
15579
  if (hadPreviousFile) {
15451
15580
  try {
15452
15581
  previous = JSON.parse(readFileSync19(resolvedPath, "utf8"));
@@ -15493,6 +15622,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15493
15622
  seats: found.seats,
15494
15623
  holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
15495
15624
  ...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
15625
+ ...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
15496
15626
  ...operator ? { operator } : {},
15497
15627
  ...contract ? { contract } : {},
15498
15628
  ...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
@@ -16351,7 +16481,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16351
16481
  );
16352
16482
  try {
16353
16483
  const evFile = eventFileFor(cfg.ship.stateDir);
16354
- const evSize = existsSync25(evFile) ? statSync9(evFile).size : 0;
16484
+ const evSize = existsSync26(evFile) ? statSync9(evFile).size : 0;
16355
16485
  process.stdout.write(
16356
16486
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
16357
16487
  ` : "events log: ok\n"
@@ -16409,12 +16539,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16409
16539
  process.stdout.write(` - ${p2}
16410
16540
  `);
16411
16541
  }
16412
- const bh = baseHealth(r);
16413
- if (bh.action !== "not-applicable") {
16414
- process.stdout.write(` base ${config.branch.base}: ${bh.unsafe ? "ERROR \u2014 " : ""}${bh.detail}
16415
- `);
16416
- if (bh.unsafe) process.exitCode = 1;
16417
- }
16542
+ const dh = doctorBaseReport(r, config.branch.base, baseHealth);
16543
+ process.stdout.write(dh.text);
16544
+ if (dh.failed) process.exitCode = 1;
16545
+ if (dh.skipRest) continue;
16418
16546
  if (config.release.ship) {
16419
16547
  process.stdout.write(` release.ship ${config.release.ship}${config.release.ship === cfg.ship.name ? " (this ship)" : ""}
16420
16548
  `);
@@ -16432,16 +16560,20 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16432
16560
  process.stdout.write(
16433
16561
  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"
16434
16562
  );
16563
+ const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
16564
+ const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
16565
+ process.stdout.write(`api key role: ${keyRole ?? "unknown"}
16566
+ `);
16435
16567
  for (const role of SEAT_KEY_ROLES) {
16436
16568
  if (!route.resolved?.seats[role]) continue;
16437
- const id = await resolveSeatKey(route, role);
16569
+ const id = await resolveSeatKey(roleRoute, role);
16438
16570
  process.stdout.write(
16439
16571
  `seat identity: ${role} writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
16440
16572
  `
16441
16573
  );
16442
16574
  }
16443
16575
  if (route.resolved?.releaseSeat) {
16444
- const id = await resolveReleaseKey(route);
16576
+ const id = await resolveReleaseKey(roleRoute);
16445
16577
  process.stdout.write(
16446
16578
  `seat identity: release writes as ${id.kind === "agent" ? `its own agent (${id.agentId})` : `the ship (${id.reason})`}
16447
16579
  `
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.50.1",
3
+ "version": "0.50.3",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",