@tablation/crew 0.58.0 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +343 -165
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -71,7 +71,11 @@ demand. `crew agents sync [route]` pushes both personas and skills in one
71
71
  run — "agents" here means every agent-shaped resource crew owns, not just
72
72
  the Agents table; `crew skills sync [route]` is the same create/update/
73
73
  diverged sync narrowed to skills alone, for when you've only touched
74
- `skills/`. Example use — a Pair session invoked via an Epic's
74
+ `skills/`. `crew connect` runs this same sync (then the seat key mint) at the
75
+ end for an admin key, so a fresh workspace needs no second command; a member
76
+ key links seats to the existing rows and is told an admin has to run it once.
77
+ `--no-agents-sync` skips it, and `--json` adds `agents_synced` and `seat_key`
78
+ events on stdout. Example use — a Pair session invoked via an Epic's
75
79
  `grill_link` looks up the `Grill-Me` skill through the Tablation MCP tool
76
80
  `agent_skills_controller_get_skill` and follows it. `prompts/default/skills/
77
81
  grill-me.md` is one shipped example; the rest are the building lanes'
package/dist/cli.js CHANGED
@@ -31,6 +31,84 @@ function doctorBaseReport(r, base, health) {
31
31
  }
32
32
  }
33
33
 
34
+ // src/doctor-checks.ts
35
+ import { execFileSync } from "child_process";
36
+ var PROBE_BRANCH = "crew-probe-branch";
37
+ var realGit = (cwd, args, env) => {
38
+ execFileSync("git", args, { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
39
+ };
40
+ function isSshRemote(url) {
41
+ return /^ssh:\/\//.test(url) || /^[\w.-]+@[\w.-]+:/.test(url);
42
+ }
43
+ function pushAuthHint(remoteUrl) {
44
+ return isSshRemote(remoteUrl) ? "make sure an SSH key is loaded for this host (ssh-add), and that the scheduler user can reach it" : "run `gh auth login`, then `gh auth setup-git`";
45
+ }
46
+ function probePushAuth(o, run2 = realGit, baseEnv = process.env) {
47
+ if (o.offline) return { state: "skipped", detail: "skipped (--offline)" };
48
+ if (!o.remoteUrl) return { state: "skipped", detail: `no remote "${o.remote}" configured` };
49
+ const env = { ...baseEnv, GIT_TERMINAL_PROMPT: "0", GCM_INTERACTIVE: "never", GIT_SSH_COMMAND: `${baseEnv.GIT_SSH_COMMAND ?? "ssh"} -o BatchMode=yes` };
50
+ try {
51
+ run2(o.dir, ["push", "--dry-run", o.remote, `HEAD:refs/heads/${PROBE_BRANCH}`], env);
52
+ return { state: "ok" };
53
+ } catch (e) {
54
+ const err = e;
55
+ const raw = (err.stderr ? String(err.stderr) : err.message).trim().split("\n").filter(Boolean).slice(-2).join(" / ");
56
+ return { state: "failed", detail: raw || "push dry-run failed", hint: pushAuthHint(o.remoteUrl) };
57
+ }
58
+ }
59
+ var INSTALL_HINTS = {
60
+ gh: "install GitHub CLI: brew install gh (https://cli.github.com)",
61
+ node: "install Node: brew install node, or https://nodejs.org",
62
+ docker: "install Docker Desktop: https://docs.docker.com/get-docker/"
63
+ };
64
+ function mentionsNode(script) {
65
+ return !!script && /\b(node|npm|pnpm|npx|yarn)\b/.test(script);
66
+ }
67
+ function isPrBased(hooks) {
68
+ return !!hooks.pr || /\bgh\b/.test(hooks.merged ?? "");
69
+ }
70
+ function toolChecks(o, find) {
71
+ const mk = (name, need) => {
72
+ const path = find(name);
73
+ return { name, need, found: !!path, ...path ? { path } : {}, hint: INSTALL_HINTS[name] };
74
+ };
75
+ const node = mk("node", "optional");
76
+ if (o.hooksMentionNode) node.hint = `your repositories' hooks may need it \u2014 ${node.hint}`;
77
+ return [mk("gh", o.prBased ? "required" : "optional"), node, mk("docker", o.hostPassengers ? "required" : "optional")];
78
+ }
79
+ function formatToolCheck(t) {
80
+ const label = `${t.name}:`.padEnd(19);
81
+ if (t.found) return `${label}OK (${t.path}) [${t.need}]
82
+ `;
83
+ return `${label}${t.need === "required" ? "NOT FOUND" : "not found"} [${t.need}] \u2014 ${t.hint}
84
+ `;
85
+ }
86
+ function formatPushAuth(repo, r) {
87
+ const label = `push auth ${repo}:`.padEnd(19);
88
+ if (r.state === "ok") return `${label}ok
89
+ `;
90
+ if (r.state === "skipped") return `${label}${r.detail}
91
+ `;
92
+ return `${label}FAILED \u2014 ${r.detail}; ${r.hint}
93
+ `;
94
+ }
95
+ async function settleRepos(items, nameOf, resolve6) {
96
+ const out = await Promise.all(items.map(async (t) => {
97
+ try {
98
+ return { ok: true, value: await resolve6(t) };
99
+ } catch (e) {
100
+ return { ok: false, repo: nameOf(t), detail: e.message };
101
+ }
102
+ }));
103
+ const settled = [];
104
+ const errors = [];
105
+ for (const o of out) {
106
+ if (o.ok) settled.push(o.value);
107
+ else errors.push({ repo: o.repo, detail: o.detail });
108
+ }
109
+ return { settled, errors };
110
+ }
111
+
34
112
  // src/selftest.ts
35
113
  import { spawn as spawn2 } from "child_process";
36
114
  import { existsSync as existsSync12, mkdirSync as mkdirSync5, mkdtempSync, readFileSync as readFileSync11, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
@@ -5104,7 +5182,7 @@ function doctorSelftestLines(stored, toolchains) {
5104
5182
  }
5105
5183
 
5106
5184
  // src/cli.ts
5107
- import { resolve as resolve5, dirname as dirname15, join as join29, basename as basename10 } from "path";
5185
+ import { resolve as resolve5, dirname as dirname16, join as join29, basename as basename10 } from "path";
5108
5186
  import { fileURLToPath } from "url";
5109
5187
  import { hostname as hostname2, homedir as homedir8 } from "os";
5110
5188
  import { spawnSync as spawnSync6 } from "child_process";
@@ -6078,7 +6156,7 @@ function recoveredFailureHeadlines(o, route) {
6078
6156
  import { existsSync as existsSync15, statfsSync } from "fs";
6079
6157
 
6080
6158
  // src/git.ts
6081
- import { execFileSync } from "child_process";
6159
+ import { execFileSync as execFileSync2 } from "child_process";
6082
6160
  import {
6083
6161
  existsSync as existsSync14,
6084
6162
  mkdirSync as mkdirSync7,
@@ -6100,7 +6178,7 @@ function streamText(x) {
6100
6178
  }
6101
6179
  function gitRaw(cwd, args) {
6102
6180
  try {
6103
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
6181
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
6104
6182
  } catch (e) {
6105
6183
  const err = e;
6106
6184
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -6109,7 +6187,7 @@ function gitRaw(cwd, args) {
6109
6187
  }
6110
6188
  function git(cwd, args) {
6111
6189
  try {
6112
- return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6190
+ return execFileSync2("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
6113
6191
  } catch (e) {
6114
6192
  const err = e;
6115
6193
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -6266,9 +6344,9 @@ function keyCommitsInRange(cwd, key, range) {
6266
6344
  }
6267
6345
  function patchIdOf(cwd, from, to) {
6268
6346
  try {
6269
- const diff = execFileSync("git", ["diff", from, to], { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
6347
+ const diff = execFileSync2("git", ["diff", from, to], { cwd, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
6270
6348
  if (!diff) return null;
6271
- const out = execFileSync("git", ["patch-id", "--stable"], { cwd, encoding: "utf8", input: diff });
6349
+ const out = execFileSync2("git", ["patch-id", "--stable"], { cwd, encoding: "utf8", input: diff });
6272
6350
  return out.split(" ")[0]?.trim() || null;
6273
6351
  } catch {
6274
6352
  return null;
@@ -6532,7 +6610,7 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
6532
6610
  mkdirSync7(dirname6(dir), { recursive: true });
6533
6611
  const url = cloneUrlFor(remote, opts);
6534
6612
  try {
6535
- execFileSync("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
6613
+ execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
6536
6614
  } catch (e) {
6537
6615
  const err = e;
6538
6616
  const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
@@ -9243,12 +9321,12 @@ import { existsSync as existsSync17 } from "fs";
9243
9321
  import { dirname as dirname7, join as join17 } from "path";
9244
9322
 
9245
9323
  // src/ports.ts
9246
- import { execFileSync as execFileSync2 } from "child_process";
9324
+ import { execFileSync as execFileSync3 } from "child_process";
9247
9325
  import { existsSync as existsSync16 } from "fs";
9248
9326
  import { join as join16 } from "path";
9249
9327
  var run = (cmd, args) => {
9250
9328
  try {
9251
- return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
9329
+ return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
9252
9330
  } catch {
9253
9331
  return "";
9254
9332
  }
@@ -12439,6 +12517,60 @@ async function canMintAgentKeys(route, o = {}) {
12439
12517
  }
12440
12518
  }
12441
12519
 
12520
+ // src/agents-sync-run.ts
12521
+ import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync12 } from "fs";
12522
+ import { dirname as dirname11 } from "path";
12523
+ async function runAgentsSync(target, o) {
12524
+ const dryRun = o.dryRun ?? false;
12525
+ const force = o.force ?? false;
12526
+ const keyRole = target.resolved?.role ?? await fetchKeyRole(target, o.userAgent);
12527
+ let readOnly = !isAdminRole(keyRole);
12528
+ const runPersonas = (ro) => syncPersonas(target, { userAgent: o.userAgent, dryRun, force, readOnly: ro });
12529
+ let personaResult;
12530
+ try {
12531
+ personaResult = await runPersonas(readOnly);
12532
+ } catch (e) {
12533
+ if (readOnly || !isForbidden(e)) throw e;
12534
+ readOnly = true;
12535
+ personaResult = await runPersonas(true);
12536
+ }
12537
+ o.say(`route: ${target.route}`);
12538
+ for (const p of personaResult.outcomes) o.say(describeSyncOutcome(p));
12539
+ for (const l of personaResult.crewLinks) o.say(describeCrewLink(l));
12540
+ if (personaResult.releaseNote) o.say(personaResult.releaseNote);
12541
+ const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
12542
+ for (const w of skillMarkerWarnings(target.promptsDir, briefs)) o.warn(`warning: ${w}`);
12543
+ let skillResult;
12544
+ if (!readOnly) {
12545
+ try {
12546
+ skillResult = await syncSkills(target, { userAgent: o.userAgent, dryRun, force });
12547
+ } catch (e) {
12548
+ if (isForbidden(e)) readOnly = true;
12549
+ else throw e;
12550
+ }
12551
+ }
12552
+ for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
12553
+ if (!dryRun) {
12554
+ const resolvedPath = resolvedPathFor(o.stateDir, target.route);
12555
+ const raw = existsSync21(resolvedPath) ? JSON.parse(readFileSync15(resolvedPath, "utf8")) : {};
12556
+ raw.agentPersonas = personaResult.agentPersonas;
12557
+ if (skillResult) raw.agentSkills = skillResult.agentSkills;
12558
+ if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
12559
+ mkdirSync10(dirname11(resolvedPath), { recursive: true });
12560
+ writeFileSync12(resolvedPath, `${JSON.stringify(raw, null, 2)}
12561
+ `);
12562
+ if (target.resolved) {
12563
+ target.resolved.agentPersonas = personaResult.agentPersonas;
12564
+ if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
12565
+ }
12566
+ }
12567
+ if (readOnly) {
12568
+ o.say("personas and skills: not synced (this key is not a workspace admin; the workspace's rows are used as they are)");
12569
+ }
12570
+ const diverged = personaResult.outcomes.some((p) => p.action === "diverged") || (skillResult?.outcomes.some((s) => s.action === "diverged") ?? false);
12571
+ return { personaResult, skillResult, readOnly, diverged };
12572
+ }
12573
+
12442
12574
  // src/logbook.ts
12443
12575
  import { createHash as createHash3 } from "crypto";
12444
12576
  import { TablationClient as TablationClient4 } from "@tablation/client";
@@ -12528,7 +12660,7 @@ async function showLogEntry(route, entryId, opts = {}) {
12528
12660
  }
12529
12661
 
12530
12662
  // src/sample-ticket.ts
12531
- import { basename as basename7, dirname as dirname11, join as join20 } from "path";
12663
+ import { basename as basename7, dirname as dirname12, join as join20 } from "path";
12532
12664
  var SAMPLE_REPO = "crew-sample";
12533
12665
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
12534
12666
  var SAMPLE_MARKER = "sample: true";
@@ -12551,7 +12683,7 @@ has something safe to work on: its test and build hooks always pass, it has no
12551
12683
  remote, and \`crew sample-ticket --remove\` deletes it.
12552
12684
  `;
12553
12685
  function sampleDir(existing, reposBase, workspace) {
12554
- if (existing.length > 0) return join20(dirname11(existing[0]), SAMPLE_REPO);
12686
+ if (existing.length > 0) return join20(dirname12(existing[0]), SAMPLE_REPO);
12555
12687
  return join20(reposBase, workspace, SAMPLE_REPO);
12556
12688
  }
12557
12689
  var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
@@ -12838,8 +12970,8 @@ function schedulerInstalled(plan, exists, crontab) {
12838
12970
  }
12839
12971
 
12840
12972
  // src/worktree-sweep.ts
12841
- import { existsSync as existsSync21, readFileSync as readFileSync15, readdirSync as readdirSync5 } from "fs";
12842
- import { basename as basename9, dirname as dirname13, join as join22, resolve as resolve4 } from "path";
12973
+ import { existsSync as existsSync22, readFileSync as readFileSync16, readdirSync as readdirSync5 } from "fs";
12974
+ import { basename as basename9, dirname as dirname14, join as join22, resolve as resolve4 } from "path";
12843
12975
  function planWorktreeSweep(target, tickets, contract) {
12844
12976
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
12845
12977
  const verified = contract.statuses.verified;
@@ -12893,7 +13025,7 @@ function worktreeTicketNumbers(target) {
12893
13025
  return [...nums];
12894
13026
  }
12895
13027
  function siblingWorktreeDirs(target) {
12896
- const parent = dirname13(target.dir.replace(/[/\\]+$/, ""));
13028
+ const parent = dirname14(target.dir.replace(/[/\\]+$/, ""));
12897
13029
  const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
12898
13030
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
12899
13031
  let names;
@@ -12941,7 +13073,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
12941
13073
  let removed = 0;
12942
13074
  let keptBranches = 0;
12943
13075
  for (const a of actions) {
12944
- if (!dryRun && base && !a.keepBranch && existsSync21(a.path) && isHandBackMerge(a.path, base)) {
13076
+ if (!dryRun && base && !a.keepBranch && existsSync22(a.path) && isHandBackMerge(a.path, base)) {
12945
13077
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
12946
13078
  log.emit(
12947
13079
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -12949,7 +13081,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
12949
13081
  );
12950
13082
  }
12951
13083
  }
12952
- if (!dryRun && existsSync21(a.path)) {
13084
+ if (!dryRun && existsSync22(a.path)) {
12953
13085
  let dirty = false;
12954
13086
  try {
12955
13087
  dirty = status(a.path).length > 0;
@@ -12960,7 +13092,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
12960
13092
  continue;
12961
13093
  }
12962
13094
  }
12963
- if (!dryRun && a.requirePushed && a.branch && existsSync21(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
13095
+ if (!dryRun && a.requirePushed && a.branch && existsSync22(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
12964
13096
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
12965
13097
  continue;
12966
13098
  }
@@ -13009,7 +13141,7 @@ function isHandBackMerge(path, base) {
13009
13141
  if (!msgPath) return false;
13010
13142
  let first;
13011
13143
  try {
13012
- first = readFileSync15(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13144
+ first = readFileSync16(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13013
13145
  } catch {
13014
13146
  return false;
13015
13147
  }
@@ -13114,12 +13246,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
13114
13246
  }
13115
13247
 
13116
13248
  // src/stream-sweep.ts
13117
- import { existsSync as existsSync22, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
13249
+ import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
13118
13250
  import { join as join23 } from "path";
13119
13251
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
13120
13252
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
13121
13253
  const dir = join23(stateDir, "streams");
13122
- if (!existsSync22(dir)) return [];
13254
+ if (!existsSync23(dir)) return [];
13123
13255
  const cutoff = now - retentionDays * DAY_MS2;
13124
13256
  return readdirSync6(dir).map((name) => join23(dir, name)).filter((path) => {
13125
13257
  try {
@@ -13147,7 +13279,7 @@ function applyStreamSweep(paths, dryRun, log) {
13147
13279
  return removed;
13148
13280
  }
13149
13281
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13150
- if (!existsSync22(stateDir)) return [];
13282
+ if (!existsSync23(stateDir)) return [];
13151
13283
  const cutoff = now - retentionDays * DAY_MS2;
13152
13284
  return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join23(stateDir, name)).filter((path) => {
13153
13285
  try {
@@ -13159,14 +13291,14 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13159
13291
  }
13160
13292
 
13161
13293
  // src/cli.ts
13162
- import { readFileSync as readFileSync21, existsSync as existsSync28, mkdirSync as mkdirSync14, writeFileSync as writeFileSync15, rmSync as rmSync11, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9, readdirSync as readdirSync11 } from "fs";
13294
+ import { readFileSync as readFileSync22, existsSync as existsSync29, mkdirSync as mkdirSync15, writeFileSync as writeFileSync16, rmSync as rmSync11, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9, readdirSync as readdirSync11 } from "fs";
13163
13295
  import { createInterface } from "readline/promises";
13164
13296
  import { dirname as dirOf, resolve as resolvePath } from "path";
13165
13297
 
13166
13298
  // src/ssh-keys.ts
13167
- import { execFileSync as execFileSync3 } from "child_process";
13168
- import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync16, chmodSync } from "fs";
13169
- import { dirname as dirname14, join as join24 } from "path";
13299
+ import { execFileSync as execFileSync4 } from "child_process";
13300
+ import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, chmodSync } from "fs";
13301
+ import { dirname as dirname15, join as join24 } from "path";
13170
13302
  var SshKeyError = class extends Error {
13171
13303
  };
13172
13304
  function shipSshKeyPathFor(stateDir) {
@@ -13175,10 +13307,10 @@ function shipSshKeyPathFor(stateDir) {
13175
13307
  function ensureShipSshKeypair(stateDir) {
13176
13308
  const privateKeyPath = shipSshKeyPathFor(stateDir);
13177
13309
  const publicKeyPath = `${privateKeyPath}.pub`;
13178
- if (!existsSync23(privateKeyPath)) {
13179
- mkdirSync10(dirname14(privateKeyPath), { recursive: true });
13310
+ if (!existsSync24(privateKeyPath)) {
13311
+ mkdirSync11(dirname15(privateKeyPath), { recursive: true });
13180
13312
  try {
13181
- execFileSync3("ssh-keygen", [
13313
+ execFileSync4("ssh-keygen", [
13182
13314
  "-t",
13183
13315
  "ed25519",
13184
13316
  "-N",
@@ -13196,15 +13328,15 @@ function ensureShipSshKeypair(stateDir) {
13196
13328
  }
13197
13329
  chmodSync(privateKeyPath, 384);
13198
13330
  }
13199
- if (!existsSync23(publicKeyPath)) {
13331
+ if (!existsSync24(publicKeyPath)) {
13200
13332
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
13201
13333
  }
13202
- const publicKey = readFileSync16(publicKeyPath, "utf8").trim();
13334
+ const publicKey = readFileSync17(publicKeyPath, "utf8").trim();
13203
13335
  return { privateKeyPath, publicKeyPath, publicKey };
13204
13336
  }
13205
13337
  function sshKeygenAvailable() {
13206
13338
  try {
13207
- execFileSync3("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
13339
+ execFileSync4("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
13208
13340
  return true;
13209
13341
  } catch (e) {
13210
13342
  return e.code !== "ENOENT";
@@ -13212,11 +13344,11 @@ function sshKeygenAvailable() {
13212
13344
  }
13213
13345
 
13214
13346
  // src/passenger-containers.ts
13215
- import { execFileSync as execFileSync4 } from "child_process";
13347
+ import { execFileSync as execFileSync5 } from "child_process";
13216
13348
  import { createHash as createHash4 } from "crypto";
13217
13349
  var realExec = (cmd, args) => {
13218
13350
  try {
13219
- return execFileSync4(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13351
+ return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13220
13352
  } catch (e) {
13221
13353
  const err = e;
13222
13354
  const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
@@ -13415,7 +13547,7 @@ function syncAllPassengerCheckouts(cfg) {
13415
13547
 
13416
13548
  // src/tunnel.ts
13417
13549
  import { spawn as spawn4 } from "child_process";
13418
- import { existsSync as existsSync24, mkdirSync as mkdirSync11, readFileSync as readFileSync17, writeFileSync as writeFileSync12, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
13550
+ import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync13, unlinkSync as unlinkSync3, readdirSync as readdirSync7 } from "fs";
13419
13551
  import { randomInt } from "crypto";
13420
13552
  import { join as join25 } from "path";
13421
13553
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -13488,17 +13620,17 @@ function tunnelStatePath(stateDir, workspaceId) {
13488
13620
  }
13489
13621
  function readPersistedTunnel(stateDir, workspaceId) {
13490
13622
  const path = tunnelStatePath(stateDir, workspaceId);
13491
- if (!existsSync24(path)) return void 0;
13623
+ if (!existsSync25(path)) return void 0;
13492
13624
  try {
13493
- return JSON.parse(readFileSync17(path, "utf8"));
13625
+ return JSON.parse(readFileSync18(path, "utf8"));
13494
13626
  } catch {
13495
13627
  return void 0;
13496
13628
  }
13497
13629
  }
13498
13630
  function writePersistedTunnel(stateDir, state) {
13499
13631
  const path = tunnelStatePath(stateDir, state.workspaceId);
13500
- mkdirSync11(join25(stateDir, "passengers"), { recursive: true });
13501
- writeFileSync12(path, `${JSON.stringify(state, null, 2)}
13632
+ mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
13633
+ writeFileSync13(path, `${JSON.stringify(state, null, 2)}
13502
13634
  `);
13503
13635
  }
13504
13636
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -13596,7 +13728,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13596
13728
  }
13597
13729
  function readAllPersistedWorkspaceIds(stateDir) {
13598
13730
  const dir = join25(stateDir, "passengers");
13599
- if (!existsSync24(dir)) return [];
13731
+ if (!existsSync25(dir)) return [];
13600
13732
  return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
13601
13733
  }
13602
13734
 
@@ -13717,7 +13849,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
13717
13849
  import { createServer as createHttpServer } from "http";
13718
13850
  import { spawn as spawn5 } from "child_process";
13719
13851
  import { randomBytes } from "crypto";
13720
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync13, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
13852
+ import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
13721
13853
  import { join as join26 } from "path";
13722
13854
 
13723
13855
  // src/secret-compare.ts
@@ -13811,17 +13943,17 @@ function syncDaemonStatePath(stateDir, workspaceId) {
13811
13943
  }
13812
13944
  function readPersistedSyncDaemon(stateDir, workspaceId) {
13813
13945
  const path = syncDaemonStatePath(stateDir, workspaceId);
13814
- if (!existsSync25(path)) return void 0;
13946
+ if (!existsSync26(path)) return void 0;
13815
13947
  try {
13816
- return JSON.parse(readFileSync18(path, "utf8"));
13948
+ return JSON.parse(readFileSync19(path, "utf8"));
13817
13949
  } catch {
13818
13950
  return void 0;
13819
13951
  }
13820
13952
  }
13821
13953
  function writePersistedSyncDaemon(stateDir, state) {
13822
13954
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
13823
- mkdirSync12(join26(stateDir, "passengers"), { recursive: true });
13824
- writeFileSync13(path, `${JSON.stringify(state, null, 2)}
13955
+ mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
13956
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
13825
13957
  `);
13826
13958
  }
13827
13959
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -13842,7 +13974,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
13842
13974
  }
13843
13975
  function readAllPersistedSyncWorkspaceIds(stateDir) {
13844
13976
  const dir = join26(stateDir, "passengers");
13845
- if (!existsSync25(dir)) return [];
13977
+ if (!existsSync26(dir)) return [];
13846
13978
  const suffix = "-sync.json";
13847
13979
  return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
13848
13980
  }
@@ -13899,11 +14031,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
13899
14031
  }
13900
14032
 
13901
14033
  // src/daemon.ts
13902
- import { existsSync as existsSync27, mkdirSync as mkdirSync13, readFileSync as readFileSync20, readdirSync as readdirSync10, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync14 } from "fs";
14034
+ import { existsSync as existsSync28, mkdirSync as mkdirSync14, readFileSync as readFileSync21, readdirSync as readdirSync10, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync15 } from "fs";
13903
14035
  import { join as join28 } from "path";
13904
14036
 
13905
14037
  // src/interrupted.ts
13906
- import { existsSync as existsSync26, readdirSync as readdirSync9, readFileSync as readFileSync19 } from "fs";
14038
+ import { existsSync as existsSync27, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
13907
14039
  import { join as join27 } from "path";
13908
14040
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
13909
14041
  function interruptedComment(b) {
@@ -13923,16 +14055,16 @@ var pidAlive = (pid) => {
13923
14055
  };
13924
14056
  function runFinished(streamPath) {
13925
14057
  const path = `${streamPath}.meta.json`;
13926
- if (!existsSync26(path)) return false;
14058
+ if (!existsSync27(path)) return false;
13927
14059
  try {
13928
- return JSON.parse(readFileSync19(path, "utf8")).running !== true;
14060
+ return JSON.parse(readFileSync20(path, "utf8")).running !== true;
13929
14061
  } catch {
13930
14062
  return true;
13931
14063
  }
13932
14064
  }
13933
14065
  function orphanedStreams(o) {
13934
14066
  const dir = join27(o.stateDir, "streams");
13935
- if (!existsSync26(dir)) return [];
14067
+ if (!existsSync27(dir)) return [];
13936
14068
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
13937
14069
  const out = [];
13938
14070
  for (const name of readdirSync9(dir)) {
@@ -14046,7 +14178,7 @@ function computeTreeSignature(paths) {
14046
14178
  }
14047
14179
  function makeStaleChecker(paths, opts = {}) {
14048
14180
  const initial = computeTreeSignature(paths);
14049
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync27(opts.reloadSentinel);
14181
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync28(opts.reloadSentinel);
14050
14182
  }
14051
14183
  function reloadSentinelFor(stateDir) {
14052
14184
  return join28(stateDir, "reload-requested");
@@ -14055,14 +14187,14 @@ function daemonStampFor(stateDir) {
14055
14187
  return join28(stateDir, "daemon.started");
14056
14188
  }
14057
14189
  function noteDaemonStarted(stateDir, now = Date.now()) {
14058
- mkdirSync13(stateDir, { recursive: true });
14190
+ mkdirSync14(stateDir, { recursive: true });
14059
14191
  rmSync10(reloadSentinelFor(stateDir), { force: true });
14060
- writeFileSync14(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14192
+ writeFileSync15(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14061
14193
  `);
14062
14194
  }
14063
14195
  function readDaemonStarted(stateDir) {
14064
14196
  try {
14065
- const v = JSON.parse(readFileSync20(daemonStampFor(stateDir), "utf8")).startedAt;
14197
+ const v = JSON.parse(readFileSync21(daemonStampFor(stateDir), "utf8")).startedAt;
14066
14198
  return typeof v === "number" ? v : null;
14067
14199
  } catch {
14068
14200
  return null;
@@ -14418,7 +14550,7 @@ async function buildEnvironment(route, ship, ticket) {
14418
14550
  }
14419
14551
 
14420
14552
  // src/cli.ts
14421
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname15(process.execPath) : resolve5(dirname15(fileURLToPath(import.meta.url)), "..");
14553
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname16(process.execPath) : resolve5(dirname16(fileURLToPath(import.meta.url)), "..");
14422
14554
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
14423
14555
  var CYCLE_SECONDS2 = 120;
14424
14556
  var STALE_CYCLES = 3;
@@ -14549,7 +14681,8 @@ function usage() {
14549
14681
  crew status [route] [--json] paused/running state; --json for a machine reader
14550
14682
  crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
14551
14683
  crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
14552
- crew doctor [route] [--fix] preflight; --fix enables clean routes and offers crew install
14684
+ crew doctor [route] [--fix] [--offline] [--json] preflight; --fix enables clean routes and offers crew install;
14685
+ --offline skips push probes; --json prints push-auth and tool checks only
14553
14686
  crew selftest [route] [--toolchain NAME] [--json] launch each toolchain as a run would and prove it reaches the tracker
14554
14687
  crew sample-ticket [route] [--remove] [--dry-run] [--json] file one ticket on a throwaway repo that walks the dev, QA and release loop
14555
14688
  crew ports [route] which checkout owns which ports, and what is up
@@ -14563,7 +14696,7 @@ function usage() {
14563
14696
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
14564
14697
  crew log [route] tail the log
14565
14698
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
14566
- crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
14699
+ crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
14567
14700
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
14568
14701
  crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
14569
14702
  crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
@@ -14976,7 +15109,7 @@ if (command === "passenger-sync-daemon") {
14976
15109
  if (host === "linux" && hasSystemd) {
14977
15110
  const label = labelFor(CREW_HOME, "run");
14978
15111
  const staleTimer = join29(homedir8(), ".config", "systemd", "user", `${label}.timer`);
14979
- if (existsSync28(staleTimer)) {
15112
+ if (existsSync29(staleTimer)) {
14980
15113
  process.stdout.write(
14981
15114
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
14982
15115
  `
@@ -15002,7 +15135,7 @@ if (command === "passenger-sync-daemon") {
15002
15135
  const tracker = new Tracker(c, cfg.ship);
15003
15136
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
15004
15137
  for (const r of await resolvedRepos(c)) {
15005
- if (!existsSync28(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15138
+ if (!existsSync29(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
15006
15139
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
15007
15140
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
15008
15141
  }
@@ -15020,7 +15153,7 @@ if (command === "passenger-sync-daemon") {
15020
15153
  }
15021
15154
  async function doctorFix(o) {
15022
15155
  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))));
15023
- const unitExists = (p) => existsSync28(p);
15156
+ const unitExists = (p) => existsSync29(p);
15024
15157
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
15025
15158
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
15026
15159
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -15037,7 +15170,7 @@ if (command === "passenger-sync-daemon") {
15037
15170
  for (const a of plan.actions) {
15038
15171
  if (a.kind === "enable") {
15039
15172
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
15040
- writeFileSync15(cfg.configFile, planEnable(readFileSync21(cfg.configFile, "utf8"), a.route));
15173
+ writeFileSync16(cfg.configFile, planEnable(readFileSync22(cfg.configFile, "utf8"), a.route));
15041
15174
  enabledSound.add(a.route);
15042
15175
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
15043
15176
  } else {
@@ -15054,7 +15187,10 @@ ${changed2.map((c) => ` - ${c}
15054
15187
  `).join("")}` : "fix: nothing changed\n");
15055
15188
  }
15056
15189
  async function resolvedRepos(c) {
15057
- return Promise.all(reposOf(c).map(async (t) => {
15190
+ return Promise.all(reposOf(c).map((t) => resolveOneRepo(c, t)));
15191
+ }
15192
+ async function resolveOneRepo(c, t) {
15193
+ {
15058
15194
  const id = repoIdForName(c, t.name);
15059
15195
  const o = c.repoOverrides[t.name];
15060
15196
  return {
@@ -15078,7 +15214,7 @@ ${changed2.map((c) => ` - ${c}
15078
15214
  worktrees: { prefix: shipWorktreePrefixFor(c) ?? await areaWorktreePrefix(c, id), nameOverride: o?.worktreeName }
15079
15215
  }, t.dir)
15080
15216
  };
15081
- }));
15217
+ }
15082
15218
  }
15083
15219
  const worktreeLocations = async (c) => (await resolvedRepos(c)).map((r) => ({
15084
15220
  parent: resolvePath(r.dir, ".."),
@@ -16160,69 +16296,27 @@ ${changed2.map((c) => ` - ${c}
16160
16296
  `);
16161
16297
  process.exit(2);
16162
16298
  }
16163
- const keyRole = target.resolved.role ?? await fetchKeyRole(target, cfg.ship.userAgent);
16164
- let readOnly = !isAdminRole(keyRole);
16165
- let personaResult;
16166
- const runPersonas = (ro) => syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force, readOnly: ro });
16299
+ let run2;
16167
16300
  try {
16168
- try {
16169
- personaResult = await runPersonas(readOnly);
16170
- } catch (e) {
16171
- if (readOnly || !isForbidden(e)) throw e;
16172
- readOnly = true;
16173
- personaResult = await runPersonas(true);
16174
- }
16301
+ run2 = await runAgentsSync(target, {
16302
+ userAgent: cfg.ship.userAgent,
16303
+ stateDir: cfg.ship.stateDir,
16304
+ dryRun,
16305
+ force,
16306
+ say: (l) => process.stdout.write(`${l}
16307
+ `),
16308
+ warn: (l) => process.stderr.write(`crew agents sync: ${l}
16309
+ `)
16310
+ });
16175
16311
  } catch (e) {
16176
- if (e instanceof AgentsSyncError) {
16312
+ if (e instanceof AgentsSyncError || e instanceof SkillSyncError) {
16177
16313
  process.stderr.write(`crew agents sync: ${e.message}
16178
16314
  `);
16179
16315
  process.exit(2);
16180
16316
  }
16181
16317
  throw e;
16182
16318
  }
16183
- process.stdout.write(`route: ${target.route}
16184
- `);
16185
- for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
16186
- `);
16187
- for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
16188
- `);
16189
- if (personaResult.releaseNote) process.stdout.write(`${personaResult.releaseNote}
16190
- `);
16191
- const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
16192
- for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
16193
- `);
16194
- let skillResult;
16195
- if (!readOnly) {
16196
- try {
16197
- skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
16198
- } catch (e) {
16199
- if (isForbidden(e)) readOnly = true;
16200
- else if (e instanceof SkillSyncError) {
16201
- process.stderr.write(`crew agents sync: ${e.message}
16202
- `);
16203
- process.exit(2);
16204
- } else throw e;
16205
- }
16206
- }
16207
- for (const o of skillResult?.outcomes ?? []) process.stdout.write(`${describeSkillSyncOutcome(o)}
16208
- `);
16209
- if (!dryRun) {
16210
- const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16211
- const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
16212
- raw.agentPersonas = personaResult.agentPersonas;
16213
- if (skillResult) raw.agentSkills = skillResult.agentSkills;
16214
- if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
16215
- mkdirSync14(dirOf(resolvedPath), { recursive: true });
16216
- writeFileSync15(resolvedPath, `${JSON.stringify(raw, null, 2)}
16217
- `);
16218
- }
16219
- if (!dryRun) {
16220
- target.resolved.agentPersonas = personaResult.agentPersonas;
16221
- if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
16222
- }
16223
- if (readOnly) {
16224
- 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");
16225
- }
16319
+ const { personaResult, skillResult } = run2;
16226
16320
  if (flag("revoke-seat-keys")) {
16227
16321
  for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
16228
16322
  process.stdout.write(`${describeRevokeOutcome(o)}
@@ -16234,9 +16328,7 @@ ${changed2.map((c) => ` - ${c}
16234
16328
  `);
16235
16329
  }
16236
16330
  }
16237
- if (personaResult.outcomes.some((o) => o.action === "diverged") || (skillResult?.outcomes.some((o) => o.action === "diverged") ?? false)) {
16238
- process.exitCode = 1;
16239
- }
16331
+ if (run2.diverged) process.exitCode = 1;
16240
16332
  break;
16241
16333
  }
16242
16334
  case "skills": {
@@ -16278,10 +16370,10 @@ ${changed2.map((c) => ` - ${c}
16278
16370
  `);
16279
16371
  if (!dryRun) {
16280
16372
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16281
- const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
16373
+ const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
16282
16374
  raw.agentSkills = result.agentSkills;
16283
- mkdirSync14(dirOf(resolvedPath), { recursive: true });
16284
- writeFileSync15(resolvedPath, `${JSON.stringify(raw, null, 2)}
16375
+ mkdirSync15(dirOf(resolvedPath), { recursive: true });
16376
+ writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
16285
16377
  `);
16286
16378
  }
16287
16379
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -16294,14 +16386,14 @@ ${changed2.map((c) => ` - ${c}
16294
16386
  process.exit(2);
16295
16387
  };
16296
16388
  if (sub !== "add" && sub !== "list" && sub !== "clone") usage2();
16297
- const configText = readFileSync21(cfg.configFile, "utf8");
16389
+ const configText = readFileSync22(cfg.configFile, "utf8");
16298
16390
  let routeName = positional[2];
16299
16391
  let addPath = ".";
16300
16392
  let addName;
16301
16393
  if (sub === "list" && !routeName) usage2();
16302
16394
  if (sub === "add") {
16303
16395
  try {
16304
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync28(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16396
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync29(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
16305
16397
  routeName = a.route;
16306
16398
  addPath = a.path;
16307
16399
  addName = a.name;
@@ -16330,15 +16422,15 @@ ${changed2.map((c) => ` - ${c}
16330
16422
  }
16331
16423
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
16332
16424
  let resolvedRemotes;
16333
- if (existsSync28(resolvedFile)) {
16425
+ if (existsSync29(resolvedFile)) {
16334
16426
  try {
16335
- resolvedRemotes = JSON.parse(readFileSync21(resolvedFile, "utf8"));
16427
+ resolvedRemotes = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16336
16428
  } catch {
16337
16429
  }
16338
16430
  }
16339
16431
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
16340
16432
  const text = configText;
16341
- const base = dirname15(cfg.configFile);
16433
+ const base = dirname16(cfg.configFile);
16342
16434
  try {
16343
16435
  if (sub === "list") {
16344
16436
  const repos = configuredRepos(text, routeName, base);
@@ -16347,7 +16439,7 @@ ${changed2.map((c) => ` - ${c}
16347
16439
  for (const r of repos) {
16348
16440
  const origin2 = originOf(r.dir);
16349
16441
  const row = matchRepoName(origin2, resolvedRemotes);
16350
- const note = !existsSync28(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16442
+ const note = !existsSync29(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
16351
16443
  process.stdout.write(`${r.name} ${r.dir} ${note}
16352
16444
  `);
16353
16445
  }
@@ -16359,9 +16451,9 @@ ${changed2.map((c) => ` - ${c}
16359
16451
  ` : `${human}
16360
16452
  `);
16361
16453
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
16362
- if (existsSync28(plan2.target) && (existsSync28(`${plan2.target}/.git`) || readdirSync11(plan2.target).length > 0)) {
16454
+ if (existsSync29(plan2.target) && (existsSync29(`${plan2.target}/.git`) || readdirSync11(plan2.target).length > 0)) {
16363
16455
  throw new ReposError(
16364
- existsSync28(`${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`
16456
+ existsSync29(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
16365
16457
  );
16366
16458
  }
16367
16459
  if (dryRun) {
@@ -16379,12 +16471,12 @@ ${changed2.map((c) => ` - ${c}
16379
16471
  throw new ReposError(`${e.message}${authHint}
16380
16472
  crew.yaml was not changed`);
16381
16473
  }
16382
- writeFileSync15(cfg.configFile, plan2.text);
16474
+ writeFileSync16(cfg.configFile, plan2.text);
16383
16475
  say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
16384
16476
  break;
16385
16477
  }
16386
16478
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
16387
- const top = existsSync28(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16479
+ const top = existsSync29(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16388
16480
  if (!top) throw new ReposError(`${target} is not a git checkout`);
16389
16481
  const origin = originOf(top);
16390
16482
  const plan = planRepoAdd({
@@ -16433,15 +16525,15 @@ ${changed2.map((c) => ` - ${c}
16433
16525
  const created = await rowTracker.createRepoRow(rowPayload);
16434
16526
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
16435
16527
  `);
16436
- if (existsSync28(resolvedFile)) {
16437
- const raw = JSON.parse(readFileSync21(resolvedFile, "utf8"));
16528
+ if (existsSync29(resolvedFile)) {
16529
+ const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16438
16530
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
16439
16531
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
16440
- writeFileSync15(resolvedFile, `${JSON.stringify(raw, null, 2)}
16532
+ writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
16441
16533
  `);
16442
16534
  }
16443
16535
  }
16444
- writeFileSync15(cfg.configFile, plan.text);
16536
+ writeFileSync16(cfg.configFile, plan.text);
16445
16537
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
16446
16538
  `);
16447
16539
  } catch (e) {
@@ -16751,10 +16843,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16751
16843
  }
16752
16844
  }
16753
16845
  let previous;
16754
- const hadPreviousFile = existsSync28(resolvedPath);
16846
+ const hadPreviousFile = existsSync29(resolvedPath);
16755
16847
  if (hadPreviousFile) {
16756
16848
  try {
16757
- previous = JSON.parse(readFileSync21(resolvedPath, "utf8"));
16849
+ previous = JSON.parse(readFileSync22(resolvedPath, "utf8"));
16758
16850
  } catch {
16759
16851
  }
16760
16852
  }
@@ -16862,10 +16954,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16862
16954
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
16863
16955
  `);
16864
16956
  } else {
16865
- mkdirSync14(dirname15(resolvedPath), { recursive: true });
16957
+ mkdirSync15(dirname16(resolvedPath), { recursive: true });
16866
16958
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
16867
16959
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
16868
- writeFileSync15(tmpPath, `${JSON.stringify(resolved, null, 2)}
16960
+ writeFileSync16(tmpPath, `${JSON.stringify(resolved, null, 2)}
16869
16961
  `);
16870
16962
  renameSync2(tmpPath, resolvedPath);
16871
16963
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -16900,8 +16992,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16900
16992
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
16901
16993
  `);
16902
16994
  } else {
16903
- mkdirSync14(dirname15(apiKeyFile), { recursive: true });
16904
- writeFileSync15(apiKeyFile, `${apiKeyVar}=${apiKey}
16995
+ mkdirSync15(dirname16(apiKeyFile), { recursive: true });
16996
+ writeFileSync16(apiKeyFile, `${apiKeyVar}=${apiKey}
16905
16997
  `, { mode: 384 });
16906
16998
  process.stderr.write(`API key written to ${apiKeyFile}
16907
16999
  `);
@@ -17002,6 +17094,56 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17002
17094
  `
17003
17095
  );
17004
17096
  }
17097
+ if (!dryRun && !flag("no-agents-sync")) {
17098
+ const skip = (why) => {
17099
+ process.stderr.write(`
17100
+ agents sync: skipped (${why})
17101
+ `);
17102
+ connectEvents.emit("agents_synced", `agents sync skipped: ${why}`, { route: route2, skipped: why });
17103
+ };
17104
+ let syncRoute;
17105
+ try {
17106
+ const reloaded = loadConfig(CREW_HOME, wizard?.configPath ?? cfg.configFile, { allowNoRoutes: true });
17107
+ syncRoute = reloaded.routes.find((r) => r.route === route2);
17108
+ if (syncRoute) await hydrateApiKeys([syncRoute]);
17109
+ } catch (e) {
17110
+ skip(`config did not load: ${e.message}`);
17111
+ }
17112
+ if (syncRoute?.resolved) {
17113
+ try {
17114
+ process.stderr.write("\n");
17115
+ const run2 = await runAgentsSync(syncRoute, {
17116
+ userAgent: cfg.ship.userAgent,
17117
+ stateDir: cfg.ship.stateDir,
17118
+ say: (l) => process.stderr.write(` ${l}
17119
+ `),
17120
+ warn: (l) => process.stderr.write(` agents sync: ${l}
17121
+ `)
17122
+ });
17123
+ const minted = await mintMissingSeatKeys(syncRoute, { userAgent: cfg.ship.userAgent, shipName: cfg.ship.name });
17124
+ for (const m of minted) process.stderr.write(` ${describeMintOutcome(m)}
17125
+ `);
17126
+ if (run2.readOnly) {
17127
+ const missing = run2.personaResult.outcomes.some((p) => p.action === "missing");
17128
+ 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");
17129
+ }
17130
+ connectEvents.emit("agents_synced", run2.readOnly ? "agents linked (read-only key)" : "agents synced", {
17131
+ route: route2,
17132
+ admin: !run2.readOnly,
17133
+ personas: run2.personaResult.outcomes,
17134
+ skills: run2.skillResult?.outcomes ?? [],
17135
+ diverged: run2.diverged
17136
+ });
17137
+ for (const m of minted) connectEvents.emit("seat_key", describeMintOutcome(m), { route: route2, ...m });
17138
+ if (run2.diverged) process.exitCode = 1;
17139
+ } catch (e) {
17140
+ if (e instanceof AgentsSyncError || e instanceof SkillSyncError) skip(e.message);
17141
+ else throw e;
17142
+ }
17143
+ } else if (!syncRoute) {
17144
+ skip(`"${route2}" is not in crew.yaml yet; add the block above, then run \`crew agents sync\``);
17145
+ }
17146
+ }
17005
17147
  connectEvents.emit("done", `connected ${route2}`, {
17006
17148
  route: route2,
17007
17149
  resolvedPath: dryRun ? void 0 : resolvedPath,
@@ -17480,8 +17622,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17480
17622
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
17481
17623
  break;
17482
17624
  }
17483
- mkdirSync14(cfg.ship.stateDir, { recursive: true });
17484
- writeFileSync15(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
17625
+ mkdirSync15(cfg.ship.stateDir, { recursive: true });
17626
+ writeFileSync16(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
17485
17627
  `);
17486
17628
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
17487
17629
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -17532,7 +17674,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17532
17674
  }
17533
17675
  case "log": {
17534
17676
  try {
17535
- const text = readFileSync21(cfg.ship.logFile, "utf8").split("\n");
17677
+ const text = readFileSync22(cfg.ship.logFile, "utf8").split("\n");
17536
17678
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
17537
17679
  `);
17538
17680
  } catch {
@@ -17555,8 +17697,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17555
17697
  };
17556
17698
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
17557
17699
  const tracker = new Tracker(route, cfg.ship);
17558
- const configText = readFileSync21(cfg.configFile, "utf8");
17559
- const cfgBase = dirname15(cfg.configFile);
17700
+ const configText = readFileSync22(cfg.configFile, "utf8");
17701
+ const cfgBase = dirname16(cfg.configFile);
17560
17702
  const configured = configuredRepos(configText, route.route, cfgBase);
17561
17703
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
17562
17704
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
@@ -17584,7 +17726,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17584
17726
  }
17585
17727
  if (configured.some((r) => r.dir === dir)) {
17586
17728
  try {
17587
- writeFileSync15(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
17729
+ writeFileSync16(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
17588
17730
  } catch (e) {
17589
17731
  if (e instanceof ReposError) fail(e.message);
17590
17732
  throw e;
@@ -17592,29 +17734,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17592
17734
  }
17593
17735
  if (row) {
17594
17736
  await tracker.deleteRepoRow(row.id);
17595
- if (existsSync28(resolvedFile)) {
17596
- const raw = JSON.parse(readFileSync21(resolvedFile, "utf8"));
17737
+ if (existsSync29(resolvedFile)) {
17738
+ const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
17597
17739
  if (raw.repoNames) delete raw.repoNames[row.id];
17598
17740
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
17599
- writeFileSync15(resolvedFile, `${JSON.stringify(raw, null, 2)}
17741
+ writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
17600
17742
  `);
17601
17743
  }
17602
17744
  }
17603
- if (existsSync28(join29(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync11(dir, { recursive: true, force: true });
17745
+ if (existsSync29(join29(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync11(dir, { recursive: true, force: true });
17604
17746
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
17605
17747
  break;
17606
17748
  }
17607
17749
  const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
17608
- const fresh = !existsSync28(join29(dir, ".git"));
17609
- if (existsSync28(dir) && fresh && readdirSync11(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
17750
+ const fresh = !existsSync29(join29(dir, ".git"));
17751
+ if (existsSync29(dir) && fresh && readdirSync11(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
17610
17752
  if (dryRun) {
17611
17753
  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`);
17612
17754
  break;
17613
17755
  }
17614
17756
  if (fresh) {
17615
- mkdirSync14(dir, { recursive: true });
17616
- writeFileSync15(join29(dir, "README.md"), SAMPLE_README);
17617
- writeFileSync15(join29(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
17757
+ mkdirSync15(dir, { recursive: true });
17758
+ writeFileSync16(join29(dir, "README.md"), SAMPLE_README);
17759
+ writeFileSync16(join29(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
17618
17760
  const g = (...a) => {
17619
17761
  const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
17620
17762
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
@@ -17632,17 +17774,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17632
17774
  spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
17633
17775
  });
17634
17776
  repoId = (await tracker.createRepoRow(payload)).id;
17635
- if (existsSync28(resolvedFile)) {
17636
- const raw = JSON.parse(readFileSync21(resolvedFile, "utf8"));
17777
+ if (existsSync29(resolvedFile)) {
17778
+ const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
17637
17779
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
17638
17780
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
17639
- writeFileSync15(resolvedFile, `${JSON.stringify(raw, null, 2)}
17781
+ writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
17640
17782
  `);
17641
17783
  }
17642
17784
  }
17643
17785
  if (!configured.some((r) => r.dir === dir)) {
17644
17786
  try {
17645
- writeFileSync15(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
17787
+ writeFileSync16(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
17646
17788
  } catch (e) {
17647
17789
  if (e instanceof ReposError) fail(e.message);
17648
17790
  throw e;
@@ -17919,6 +18061,40 @@ watermark: ${state.watermark()}
17919
18061
  }
17920
18062
  case "doctor": {
17921
18063
  const host = hostPlatform();
18064
+ const doctorSchedulerPath = pathFor(cfg.ship);
18065
+ const pushAuth = [];
18066
+ const toolRows = [];
18067
+ {
18068
+ const prBased = /* @__PURE__ */ new Set();
18069
+ let mentionNode = false;
18070
+ for (const c of [route]) {
18071
+ const { settled: repos, errors } = await settleRepos(reposOf(c), (t) => t.name, (t) => resolveOneRepo(c, t));
18072
+ for (const e of errors) {
18073
+ 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" } });
18074
+ }
18075
+ for (const r of repos) {
18076
+ const remote = r.config.branch.remote;
18077
+ if (existsSync29(r.dir)) {
18078
+ pushAuth.push({
18079
+ route: c.route,
18080
+ repo: r.name,
18081
+ result: probePushAuth({ dir: r.dir, remote, remoteUrl: originUrlOf(r.dir, remote), offline: flag("offline") })
18082
+ });
18083
+ }
18084
+ if (isPrBased(r.config.hooks)) prBased.add(r.name);
18085
+ if (Object.values(r.config.hooks).some((h) => typeof h === "string" && mentionsNode(h))) mentionNode = true;
18086
+ }
18087
+ }
18088
+ toolRows.push(...toolChecks(
18089
+ { prBased: prBased.size > 0, hostPassengers: cfg.routes.some((x) => x.enabled && x.hostPassengers), hooksMentionNode: mentionNode },
18090
+ (bin) => findOnPath(bin, doctorSchedulerPath)
18091
+ ));
18092
+ }
18093
+ if (flag("json")) {
18094
+ process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows }, null, 2)}
18095
+ `);
18096
+ break;
18097
+ }
17922
18098
  process.stdout.write(
17923
18099
  `ship: ${cfg.ship.name} (${host})
17924
18100
  routes: ${cfg.routes.map((c) => c.route).join(", ")}
@@ -17926,7 +18102,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
17926
18102
  );
17927
18103
  try {
17928
18104
  const evFile = eventFileFor(cfg.ship.stateDir);
17929
- const evSize = existsSync28(evFile) ? statSync9(evFile).size : 0;
18105
+ const evSize = existsSync29(evFile) ? statSync9(evFile).size : 0;
17930
18106
  process.stdout.write(
17931
18107
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
17932
18108
  ` : "events log: ok\n"
@@ -17937,7 +18113,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
17937
18113
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
17938
18114
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
17939
18115
  try {
17940
- return readFileSync21(p, "utf8");
18116
+ return readFileSync22(p, "utf8");
17941
18117
  } catch {
17942
18118
  return null;
17943
18119
  }
@@ -18142,6 +18318,8 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
18142
18318
  if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
18143
18319
  `);
18144
18320
  }
18321
+ for (const p of pushAuth) process.stdout.write(formatPushAuth(`${p.repo}`, p.result));
18322
+ for (const t of toolRows) process.stdout.write(formatToolCheck(t));
18145
18323
  process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
18146
18324
  `);
18147
18325
  if (!allBinsFound || !gitPath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.58.0",
3
+ "version": "0.59.0",
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",