@tablation/crew 0.57.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 +6 -1
  2. package/dist/cli.js +583 -180
  3. package/package.json +1 -1
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 dirname14, join as join28, basename as basename9 } 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(" / ");
@@ -7201,6 +7279,25 @@ var Tracker = class {
7201
7279
  if (!model) throw new Error("this workspace has no Repos table");
7202
7280
  return this.client.records.create(model, fields);
7203
7281
  }
7282
+ /** Every `Repos` row as `{ id, name }`, for finding one by name. */
7283
+ async repoRows() {
7284
+ const model = this.route.resolved?.reposModelId;
7285
+ if (!model) return [];
7286
+ return this.client.records.list(model, { limit: 200 });
7287
+ }
7288
+ /** Delete a `Repos` row (`crew sample-ticket --remove`). */
7289
+ async deleteRepoRow(id) {
7290
+ const model = this.route.resolved?.reposModelId;
7291
+ if (!model) throw new Error("this workspace has no Repos table");
7292
+ await this.client.records.remove(model, id);
7293
+ }
7294
+ /** Every ticket filed against one `Repos` row, any status. */
7295
+ async ticketsForRepo(repoId) {
7296
+ return this.client.records.list(this.models.issues, {
7297
+ filters: encodeFilters([{ columnName: this.contract.columns.repo, operator: "EQ", value: repoId }]),
7298
+ limit: 200
7299
+ });
7300
+ }
7204
7301
  /**
7205
7302
  * The Ships rows of this workspace, if it has a Ships table.
7206
7303
  *
@@ -9224,12 +9321,12 @@ import { existsSync as existsSync17 } from "fs";
9224
9321
  import { dirname as dirname7, join as join17 } from "path";
9225
9322
 
9226
9323
  // src/ports.ts
9227
- import { execFileSync as execFileSync2 } from "child_process";
9324
+ import { execFileSync as execFileSync3 } from "child_process";
9228
9325
  import { existsSync as existsSync16 } from "fs";
9229
9326
  import { join as join16 } from "path";
9230
9327
  var run = (cmd, args) => {
9231
9328
  try {
9232
- return execFileSync2(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
9329
+ return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
9233
9330
  } catch {
9234
9331
  return "";
9235
9332
  }
@@ -12420,6 +12517,60 @@ async function canMintAgentKeys(route, o = {}) {
12420
12517
  }
12421
12518
  }
12422
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
+
12423
12574
  // src/logbook.ts
12424
12575
  import { createHash as createHash3 } from "crypto";
12425
12576
  import { TablationClient as TablationClient4 } from "@tablation/client";
@@ -12508,9 +12659,77 @@ async function showLogEntry(route, entryId, opts = {}) {
12508
12659
  };
12509
12660
  }
12510
12661
 
12662
+ // src/sample-ticket.ts
12663
+ import { basename as basename7, dirname as dirname12, join as join20 } from "path";
12664
+ var SAMPLE_REPO = "crew-sample";
12665
+ var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
12666
+ var SAMPLE_MARKER = "sample: true";
12667
+ var SAMPLE_CREW_YAML = `version: 1
12668
+ platform: unix
12669
+ branch:
12670
+ base: main
12671
+ hooks:
12672
+ test: "true"
12673
+ build: "true"
12674
+ release:
12675
+ mode: integrate
12676
+ versioning: none
12677
+ changelog: false
12678
+ `;
12679
+ var SAMPLE_README = `# crew-sample
12680
+
12681
+ A throwaway repository created by \`crew sample-ticket\`. It exists so the crew
12682
+ has something safe to work on: its test and build hooks always pass, it has no
12683
+ remote, and \`crew sample-ticket --remove\` deletes it.
12684
+ `;
12685
+ function sampleDir(existing, reposBase, workspace) {
12686
+ if (existing.length > 0) return join20(dirname12(existing[0]), SAMPLE_REPO);
12687
+ return join20(reposBase, workspace, SAMPLE_REPO);
12688
+ }
12689
+ var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
12690
+ function sampleDescription() {
12691
+ return [
12692
+ SAMPLE_MARKER,
12693
+ "",
12694
+ "> This is a sample ticket filed by `crew sample-ticket` on a throwaway repository. Nothing here is real work.",
12695
+ "",
12696
+ "## What",
12697
+ "",
12698
+ "Add `greet.js` exporting `greet(name)`, which returns `Hello, <name>!` (and `Hello, world!` for no name),",
12699
+ "with a JSDoc comment, and `greet.test.js` covering both cases using `node:test`.",
12700
+ "",
12701
+ "## Acceptance",
12702
+ "",
12703
+ '- `greet("Ada")` returns `Hello, Ada!`; `greet()` returns `Hello, world!`.',
12704
+ "- The test file runs with `node --test` and passes.",
12705
+ "- Add a line to the README saying the function exists.",
12706
+ "",
12707
+ "## Notes",
12708
+ "",
12709
+ "The repo has no remote: leave the branch local. The hooks in `.crew.yaml` always pass, so QA is checking the",
12710
+ "diff against the acceptance list above."
12711
+ ].join("\n");
12712
+ }
12713
+ function isSampleTicket(description) {
12714
+ return (description ?? "").trimStart().startsWith(SAMPLE_MARKER);
12715
+ }
12716
+ function planSampleTicket(i) {
12717
+ return {
12718
+ title: SAMPLE_TITLE,
12719
+ description: sampleDescription(),
12720
+ report_type: "feature",
12721
+ severity: "s4",
12722
+ reporter_name: "crew sample-ticket",
12723
+ status: i.approvedStatus,
12724
+ project_id: i.projectId,
12725
+ repo_id: i.repoId,
12726
+ ...i.filedBy ? { filed_by_id: i.filedBy } : {}
12727
+ };
12728
+ }
12729
+
12511
12730
  // src/repos-cmd.ts
12512
12731
  import { homedir as homedir7 } from "os";
12513
- import { basename as basename7, join as join20, resolve as resolve3 } from "path";
12732
+ import { basename as basename8, join as join21, resolve as resolve3 } from "path";
12514
12733
  import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
12515
12734
  var ReposError = class extends Error {
12516
12735
  };
@@ -12582,7 +12801,7 @@ function configuredRepos(text, route, base) {
12582
12801
  });
12583
12802
  }
12584
12803
  const dir = m.get("dir");
12585
- return dir ? [{ name: basename7(String(dir)), dir: expandDir(String(dir), base) }] : [];
12804
+ return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
12586
12805
  }
12587
12806
  function planRepoAdd(i) {
12588
12807
  const doc = parseDocument(i.text);
@@ -12594,7 +12813,7 @@ function planRepoAdd(i) {
12594
12813
  throw new ReposError(`${target} is already configured on route ${i.route}`);
12595
12814
  }
12596
12815
  const matched = matchRepoName(i.origin, i.resolved);
12597
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename7(target);
12816
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
12598
12817
  let warning;
12599
12818
  if (!matched) {
12600
12819
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -12621,6 +12840,28 @@ function planRepoAdd(i) {
12621
12840
  }
12622
12841
  return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched };
12623
12842
  }
12843
+ function planRepoRemove(text, base, route, path) {
12844
+ const doc = parseDocument(text);
12845
+ if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
12846
+ const m = routeMapOf(doc, route);
12847
+ const target = expandDir(path, base);
12848
+ const existing = configuredRepos(text, route, base);
12849
+ const hit = existing.find((e) => e.dir === target);
12850
+ if (!hit) throw new ReposError(`${target} is not configured on route ${route}`);
12851
+ const repos = m.get("repos", true);
12852
+ if (isMap(repos)) {
12853
+ repos.delete(hit.name);
12854
+ const left = existing.filter((e) => e !== hit);
12855
+ if (left.length === 1) {
12856
+ const pair = m.items.find((p) => p.key.value === "repos");
12857
+ pair.key = doc.createNode("dir");
12858
+ pair.value = doc.createNode(String(repos.get(left[0].name)));
12859
+ } else if (left.length === 0) m.delete("repos");
12860
+ } else {
12861
+ m.delete("dir");
12862
+ }
12863
+ return { text: String(doc), name: hit.name };
12864
+ }
12624
12865
  function renderBlock(m) {
12625
12866
  return stringify2([m.toJSON()]).trimEnd();
12626
12867
  }
@@ -12686,7 +12927,7 @@ function planRepoClone(i) {
12686
12927
  }
12687
12928
  const ship = doc.get("ship", true);
12688
12929
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
12689
- const target = i.dir ? expandDir(i.dir, i.base) : join20(expandDir(basePath, i.base), i.route.split("/")[0], name);
12930
+ const target = i.dir ? expandDir(i.dir, i.base) : join21(expandDir(basePath, i.base), i.route.split("/")[0], name);
12690
12931
  const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
12691
12932
  return { ...add, remote, target };
12692
12933
  }
@@ -12729,8 +12970,8 @@ function schedulerInstalled(plan, exists, crontab) {
12729
12970
  }
12730
12971
 
12731
12972
  // src/worktree-sweep.ts
12732
- import { existsSync as existsSync21, readFileSync as readFileSync15, readdirSync as readdirSync5 } from "fs";
12733
- import { basename as basename8, dirname as dirname12, join as join21, 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";
12734
12975
  function planWorktreeSweep(target, tickets, contract) {
12735
12976
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
12736
12977
  const verified = contract.statuses.verified;
@@ -12761,7 +13002,7 @@ function ticketForWorktree(w, byNumber, contract) {
12761
13002
  const t = num ? byNumber.get(num) : void 0;
12762
13003
  if (t && ticketStems(t).has(stem)) return t;
12763
13004
  }
12764
- const dir = basename8(w.path);
13005
+ const dir = basename9(w.path);
12765
13006
  const m = DIR_NUMBER.exec(dir);
12766
13007
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
12767
13008
  if (!w.branch) {
@@ -12777,15 +13018,15 @@ function worktreeTicketNumbers(target) {
12777
13018
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
12778
13019
  if (n) nums.add(n);
12779
13020
  }
12780
- const m = DIR_NUMBER_SUFFIXED.exec(basename8(w.path));
13021
+ const m = DIR_NUMBER_SUFFIXED.exec(basename9(w.path));
12781
13022
  if (m) nums.add(m[1]);
12782
13023
  }
12783
13024
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
12784
13025
  return [...nums];
12785
13026
  }
12786
13027
  function siblingWorktreeDirs(target) {
12787
- const parent = dirname12(target.dir.replace(/[/\\]+$/, ""));
12788
- const stem = `${basename8(target.dir.replace(/[/\\]+$/, ""))}-`;
13028
+ const parent = dirname14(target.dir.replace(/[/\\]+$/, ""));
13029
+ const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
12789
13030
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
12790
13031
  let names;
12791
13032
  try {
@@ -12797,7 +13038,7 @@ function siblingWorktreeDirs(target) {
12797
13038
  for (const name of names) {
12798
13039
  if (!name.startsWith(stem)) continue;
12799
13040
  const m = DIR_NUMBER_SUFFIXED.exec(name);
12800
- const path = join21(parent, name);
13041
+ const path = join22(parent, name);
12801
13042
  if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
12802
13043
  }
12803
13044
  return out;
@@ -12810,7 +13051,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
12810
13051
  }
12811
13052
  function warnOrphanedWorktreeDirs(paths, log, held) {
12812
13053
  for (const p of paths) {
12813
- const number = DIR_NUMBER_SUFFIXED.exec(basename8(p))?.[1];
13054
+ const number = DIR_NUMBER_SUFFIXED.exec(basename9(p))?.[1];
12814
13055
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
12815
13056
  if (when) {
12816
13057
  log.warn(`orphaned directory ${p}: its ticket is deployed but it is not a registered worktree; the sweep will not delete it \u2014 remove it by hand`, { step: "worktree" });
@@ -12832,7 +13073,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
12832
13073
  let removed = 0;
12833
13074
  let keptBranches = 0;
12834
13075
  for (const a of actions) {
12835
- 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)) {
12836
13077
  if (gitOk(a.path, ["merge", "--abort"]) !== null) {
12837
13078
  log.emit(
12838
13079
  `abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
@@ -12840,7 +13081,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
12840
13081
  );
12841
13082
  }
12842
13083
  }
12843
- if (!dryRun && existsSync21(a.path)) {
13084
+ if (!dryRun && existsSync22(a.path)) {
12844
13085
  let dirty = false;
12845
13086
  try {
12846
13087
  dirty = status(a.path).length > 0;
@@ -12851,7 +13092,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
12851
13092
  continue;
12852
13093
  }
12853
13094
  }
12854
- 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)) {
12855
13096
  refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
12856
13097
  continue;
12857
13098
  }
@@ -12900,7 +13141,7 @@ function isHandBackMerge(path, base) {
12900
13141
  if (!msgPath) return false;
12901
13142
  let first;
12902
13143
  try {
12903
- first = readFileSync15(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
13144
+ first = readFileSync16(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
12904
13145
  } catch {
12905
13146
  return false;
12906
13147
  }
@@ -12954,7 +13195,7 @@ function isTicketBranch(name, base, stems) {
12954
13195
  }
12955
13196
  function sweepCandidatesExist(cwd, base) {
12956
13197
  const linked = worktrees(cwd).slice(1);
12957
- if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename8(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
13198
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename9(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
12958
13199
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
12959
13200
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
12960
13201
  }
@@ -13005,14 +13246,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
13005
13246
  }
13006
13247
 
13007
13248
  // src/stream-sweep.ts
13008
- import { existsSync as existsSync22, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
13009
- import { join as join22 } from "path";
13249
+ import { existsSync as existsSync23, readdirSync as readdirSync6, rmSync as rmSync9, statSync as statSync7 } from "fs";
13250
+ import { join as join23 } from "path";
13010
13251
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
13011
13252
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
13012
- const dir = join22(stateDir, "streams");
13013
- if (!existsSync22(dir)) return [];
13253
+ const dir = join23(stateDir, "streams");
13254
+ if (!existsSync23(dir)) return [];
13014
13255
  const cutoff = now - retentionDays * DAY_MS2;
13015
- return readdirSync6(dir).map((name) => join22(dir, name)).filter((path) => {
13256
+ return readdirSync6(dir).map((name) => join23(dir, name)).filter((path) => {
13016
13257
  try {
13017
13258
  return statSync7(path).mtimeMs < cutoff;
13018
13259
  } catch {
@@ -13038,9 +13279,9 @@ function applyStreamSweep(paths, dryRun, log) {
13038
13279
  return removed;
13039
13280
  }
13040
13281
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13041
- if (!existsSync22(stateDir)) return [];
13282
+ if (!existsSync23(stateDir)) return [];
13042
13283
  const cutoff = now - retentionDays * DAY_MS2;
13043
- return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join22(stateDir, name)).filter((path) => {
13284
+ return readdirSync6(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join23(stateDir, name)).filter((path) => {
13044
13285
  try {
13045
13286
  return statSync7(path).mtimeMs < cutoff;
13046
13287
  } catch {
@@ -13050,26 +13291,26 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
13050
13291
  }
13051
13292
 
13052
13293
  // src/cli.ts
13053
- import { readFileSync as readFileSync21, existsSync as existsSync28, mkdirSync as mkdirSync14, writeFileSync as writeFileSync15, 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";
13054
13295
  import { createInterface } from "readline/promises";
13055
13296
  import { dirname as dirOf, resolve as resolvePath } from "path";
13056
13297
 
13057
13298
  // src/ssh-keys.ts
13058
- import { execFileSync as execFileSync3 } from "child_process";
13059
- import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync16, chmodSync } from "fs";
13060
- import { dirname as dirname13, join as join23 } 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";
13061
13302
  var SshKeyError = class extends Error {
13062
13303
  };
13063
13304
  function shipSshKeyPathFor(stateDir) {
13064
- return join23(stateDir, "ssh", "id_ed25519");
13305
+ return join24(stateDir, "ssh", "id_ed25519");
13065
13306
  }
13066
13307
  function ensureShipSshKeypair(stateDir) {
13067
13308
  const privateKeyPath = shipSshKeyPathFor(stateDir);
13068
13309
  const publicKeyPath = `${privateKeyPath}.pub`;
13069
- if (!existsSync23(privateKeyPath)) {
13070
- mkdirSync10(dirname13(privateKeyPath), { recursive: true });
13310
+ if (!existsSync24(privateKeyPath)) {
13311
+ mkdirSync11(dirname15(privateKeyPath), { recursive: true });
13071
13312
  try {
13072
- execFileSync3("ssh-keygen", [
13313
+ execFileSync4("ssh-keygen", [
13073
13314
  "-t",
13074
13315
  "ed25519",
13075
13316
  "-N",
@@ -13087,15 +13328,15 @@ function ensureShipSshKeypair(stateDir) {
13087
13328
  }
13088
13329
  chmodSync(privateKeyPath, 384);
13089
13330
  }
13090
- if (!existsSync23(publicKeyPath)) {
13331
+ if (!existsSync24(publicKeyPath)) {
13091
13332
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
13092
13333
  }
13093
- const publicKey = readFileSync16(publicKeyPath, "utf8").trim();
13334
+ const publicKey = readFileSync17(publicKeyPath, "utf8").trim();
13094
13335
  return { privateKeyPath, publicKeyPath, publicKey };
13095
13336
  }
13096
13337
  function sshKeygenAvailable() {
13097
13338
  try {
13098
- execFileSync3("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
13339
+ execFileSync4("ssh-keygen", ["-V"], { stdio: ["ignore", "ignore", "ignore"] });
13099
13340
  return true;
13100
13341
  } catch (e) {
13101
13342
  return e.code !== "ENOENT";
@@ -13103,11 +13344,11 @@ function sshKeygenAvailable() {
13103
13344
  }
13104
13345
 
13105
13346
  // src/passenger-containers.ts
13106
- import { execFileSync as execFileSync4 } from "child_process";
13347
+ import { execFileSync as execFileSync5 } from "child_process";
13107
13348
  import { createHash as createHash4 } from "crypto";
13108
13349
  var realExec = (cmd, args) => {
13109
13350
  try {
13110
- return execFileSync4(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13351
+ return execFileSync5(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13111
13352
  } catch (e) {
13112
13353
  const err = e;
13113
13354
  const why = [err.stderr, err.stdout].map((x) => (x ?? "").trim()).filter(Boolean).join(" / ");
@@ -13306,9 +13547,9 @@ function syncAllPassengerCheckouts(cfg) {
13306
13547
 
13307
13548
  // src/tunnel.ts
13308
13549
  import { spawn as spawn4 } from "child_process";
13309
- 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";
13310
13551
  import { randomInt } from "crypto";
13311
- import { join as join24 } from "path";
13552
+ import { join as join25 } from "path";
13312
13553
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
13313
13554
  var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
13314
13555
  var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
@@ -13375,21 +13616,21 @@ function startTunnel(opts) {
13375
13616
  };
13376
13617
  }
13377
13618
  function tunnelStatePath(stateDir, workspaceId) {
13378
- return join24(stateDir, "passengers", `${workspaceId}.json`);
13619
+ return join25(stateDir, "passengers", `${workspaceId}.json`);
13379
13620
  }
13380
13621
  function readPersistedTunnel(stateDir, workspaceId) {
13381
13622
  const path = tunnelStatePath(stateDir, workspaceId);
13382
- if (!existsSync24(path)) return void 0;
13623
+ if (!existsSync25(path)) return void 0;
13383
13624
  try {
13384
- return JSON.parse(readFileSync17(path, "utf8"));
13625
+ return JSON.parse(readFileSync18(path, "utf8"));
13385
13626
  } catch {
13386
13627
  return void 0;
13387
13628
  }
13388
13629
  }
13389
13630
  function writePersistedTunnel(stateDir, state) {
13390
13631
  const path = tunnelStatePath(stateDir, state.workspaceId);
13391
- mkdirSync11(join24(stateDir, "passengers"), { recursive: true });
13392
- writeFileSync12(path, `${JSON.stringify(state, null, 2)}
13632
+ mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
13633
+ writeFileSync13(path, `${JSON.stringify(state, null, 2)}
13393
13634
  `);
13394
13635
  }
13395
13636
  function updatePersistedTunnel(stateDir, workspaceId, patch2) {
@@ -13486,8 +13727,8 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
13486
13727
  }
13487
13728
  }
13488
13729
  function readAllPersistedWorkspaceIds(stateDir) {
13489
- const dir = join24(stateDir, "passengers");
13490
- if (!existsSync24(dir)) return [];
13730
+ const dir = join25(stateDir, "passengers");
13731
+ if (!existsSync25(dir)) return [];
13491
13732
  return readdirSync7(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
13492
13733
  }
13493
13734
 
@@ -13608,8 +13849,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
13608
13849
  import { createServer as createHttpServer } from "http";
13609
13850
  import { spawn as spawn5 } from "child_process";
13610
13851
  import { randomBytes } from "crypto";
13611
- import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync13, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
13612
- import { join as join25 } from "path";
13852
+ import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync4, readdirSync as readdirSync8 } from "fs";
13853
+ import { join as join26 } from "path";
13613
13854
 
13614
13855
  // src/secret-compare.ts
13615
13856
  import { timingSafeEqual } from "crypto";
@@ -13698,21 +13939,21 @@ var defaultSpawn2 = (cmd, args, env) => {
13698
13939
  return child;
13699
13940
  };
13700
13941
  function syncDaemonStatePath(stateDir, workspaceId) {
13701
- return join25(stateDir, "passengers", `${workspaceId}-sync.json`);
13942
+ return join26(stateDir, "passengers", `${workspaceId}-sync.json`);
13702
13943
  }
13703
13944
  function readPersistedSyncDaemon(stateDir, workspaceId) {
13704
13945
  const path = syncDaemonStatePath(stateDir, workspaceId);
13705
- if (!existsSync25(path)) return void 0;
13946
+ if (!existsSync26(path)) return void 0;
13706
13947
  try {
13707
- return JSON.parse(readFileSync18(path, "utf8"));
13948
+ return JSON.parse(readFileSync19(path, "utf8"));
13708
13949
  } catch {
13709
13950
  return void 0;
13710
13951
  }
13711
13952
  }
13712
13953
  function writePersistedSyncDaemon(stateDir, state) {
13713
13954
  const path = syncDaemonStatePath(stateDir, state.workspaceId);
13714
- mkdirSync12(join25(stateDir, "passengers"), { recursive: true });
13715
- writeFileSync13(path, `${JSON.stringify(state, null, 2)}
13955
+ mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
13956
+ writeFileSync14(path, `${JSON.stringify(state, null, 2)}
13716
13957
  `);
13717
13958
  }
13718
13959
  function removePersistedSyncDaemon(stateDir, workspaceId) {
@@ -13732,8 +13973,8 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
13732
13973
  }
13733
13974
  }
13734
13975
  function readAllPersistedSyncWorkspaceIds(stateDir) {
13735
- const dir = join25(stateDir, "passengers");
13736
- if (!existsSync25(dir)) return [];
13976
+ const dir = join26(stateDir, "passengers");
13977
+ if (!existsSync26(dir)) return [];
13737
13978
  const suffix = "-sync.json";
13738
13979
  return readdirSync8(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
13739
13980
  }
@@ -13790,12 +14031,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
13790
14031
  }
13791
14032
 
13792
14033
  // src/daemon.ts
13793
- import { existsSync as existsSync27, mkdirSync as mkdirSync13, readFileSync as readFileSync20, readdirSync as readdirSync10, rmSync as rmSync10, statSync as statSync8, writeFileSync as writeFileSync14 } from "fs";
13794
- import { join as join27 } from "path";
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";
14035
+ import { join as join28 } from "path";
13795
14036
 
13796
14037
  // src/interrupted.ts
13797
- import { existsSync as existsSync26, readdirSync as readdirSync9, readFileSync as readFileSync19 } from "fs";
13798
- import { join as join26 } from "path";
14038
+ import { existsSync as existsSync27, readdirSync as readdirSync9, readFileSync as readFileSync20 } from "fs";
14039
+ import { join as join27 } from "path";
13799
14040
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
13800
14041
  function interruptedComment(b) {
13801
14042
  const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
@@ -13814,21 +14055,21 @@ var pidAlive = (pid) => {
13814
14055
  };
13815
14056
  function runFinished(streamPath) {
13816
14057
  const path = `${streamPath}.meta.json`;
13817
- if (!existsSync26(path)) return false;
14058
+ if (!existsSync27(path)) return false;
13818
14059
  try {
13819
- return JSON.parse(readFileSync19(path, "utf8")).running !== true;
14060
+ return JSON.parse(readFileSync20(path, "utf8")).running !== true;
13820
14061
  } catch {
13821
14062
  return true;
13822
14063
  }
13823
14064
  }
13824
14065
  function orphanedStreams(o) {
13825
- const dir = join26(o.stateDir, "streams");
13826
- if (!existsSync26(dir)) return [];
14066
+ const dir = join27(o.stateDir, "streams");
14067
+ if (!existsSync27(dir)) return [];
13827
14068
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
13828
14069
  const out = [];
13829
14070
  for (const name of readdirSync9(dir)) {
13830
14071
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
13831
- const streamPath = join26(dir, name);
14072
+ const streamPath = join27(dir, name);
13832
14073
  if (runFinished(streamPath)) continue;
13833
14074
  const sidecar = readRunSidecar(streamPath);
13834
14075
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
@@ -13928,7 +14169,7 @@ function latestMtime(path) {
13928
14169
  if (!stat.isDirectory()) return 0;
13929
14170
  let latest = stat.mtimeMs;
13930
14171
  for (const entry of readdirSync10(path, { withFileTypes: true })) {
13931
- latest = Math.max(latest, latestMtime(join27(path, entry.name)));
14172
+ latest = Math.max(latest, latestMtime(join28(path, entry.name)));
13932
14173
  }
13933
14174
  return latest;
13934
14175
  }
@@ -13937,23 +14178,23 @@ function computeTreeSignature(paths) {
13937
14178
  }
13938
14179
  function makeStaleChecker(paths, opts = {}) {
13939
14180
  const initial = computeTreeSignature(paths);
13940
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync27(opts.reloadSentinel);
14181
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync28(opts.reloadSentinel);
13941
14182
  }
13942
14183
  function reloadSentinelFor(stateDir) {
13943
- return join27(stateDir, "reload-requested");
14184
+ return join28(stateDir, "reload-requested");
13944
14185
  }
13945
14186
  function daemonStampFor(stateDir) {
13946
- return join27(stateDir, "daemon.started");
14187
+ return join28(stateDir, "daemon.started");
13947
14188
  }
13948
14189
  function noteDaemonStarted(stateDir, now = Date.now()) {
13949
- mkdirSync13(stateDir, { recursive: true });
14190
+ mkdirSync14(stateDir, { recursive: true });
13950
14191
  rmSync10(reloadSentinelFor(stateDir), { force: true });
13951
- writeFileSync14(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
14192
+ writeFileSync15(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
13952
14193
  `);
13953
14194
  }
13954
14195
  function readDaemonStarted(stateDir) {
13955
14196
  try {
13956
- const v = JSON.parse(readFileSync20(daemonStampFor(stateDir), "utf8")).startedAt;
14197
+ const v = JSON.parse(readFileSync21(daemonStampFor(stateDir), "utf8")).startedAt;
13957
14198
  return typeof v === "number" ? v : null;
13958
14199
  } catch {
13959
14200
  return null;
@@ -14309,7 +14550,7 @@ async function buildEnvironment(route, ship, ticket) {
14309
14550
  }
14310
14551
 
14311
14552
  // src/cli.ts
14312
- var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname14(process.execPath) : resolve5(dirname14(fileURLToPath(import.meta.url)), "..");
14553
+ var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname16(process.execPath) : resolve5(dirname16(fileURLToPath(import.meta.url)), "..");
14313
14554
  var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
14314
14555
  var CYCLE_SECONDS2 = 120;
14315
14556
  var STALE_CYCLES = 3;
@@ -14440,8 +14681,10 @@ function usage() {
14440
14681
  crew status [route] [--json] paused/running state; --json for a machine reader
14441
14682
  crew projects [--route R] [--json] the Projects rows each route can read (id, name, prefix, work mode)
14442
14683
  crew reload make the running daemon re-read crew.yaml (--now: restart it immediately)
14443
- 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
14444
14686
  crew selftest [route] [--toolchain NAME] [--json] launch each toolchain as a run would and prove it reaches the tracker
14687
+ crew sample-ticket [route] [--remove] [--dry-run] [--json] file one ticket on a throwaway repo that walks the dev, QA and release loop
14445
14688
  crew ports [route] which checkout owns which ports, and what is up
14446
14689
  crew reap [route] kill orphaned servers, drop worktrees for closed tickets
14447
14690
  crew drop [route] NNN remove a merged ticket's worktree and branch
@@ -14453,7 +14696,7 @@ function usage() {
14453
14696
  resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
14454
14697
  crew log [route] tail the log
14455
14698
  crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
14456
- 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)
14457
14700
  crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
14458
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)
14459
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)
@@ -14865,8 +15108,8 @@ if (command === "passenger-sync-daemon") {
14865
15108
  }
14866
15109
  if (host === "linux" && hasSystemd) {
14867
15110
  const label = labelFor(CREW_HOME, "run");
14868
- const staleTimer = join28(homedir8(), ".config", "systemd", "user", `${label}.timer`);
14869
- if (existsSync28(staleTimer)) {
15111
+ const staleTimer = join29(homedir8(), ".config", "systemd", "user", `${label}.timer`);
15112
+ if (existsSync29(staleTimer)) {
14870
15113
  process.stdout.write(
14871
15114
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
14872
15115
  `
@@ -14892,7 +15135,7 @@ if (command === "passenger-sync-daemon") {
14892
15135
  const tracker = new Tracker(c, cfg.ship);
14893
15136
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
14894
15137
  for (const r of await resolvedRepos(c)) {
14895
- 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`);
14896
15139
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
14897
15140
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
14898
15141
  }
@@ -14910,7 +15153,7 @@ if (command === "passenger-sync-daemon") {
14910
15153
  }
14911
15154
  async function doctorFix(o) {
14912
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))));
14913
- const unitExists = (p) => existsSync28(p);
15156
+ const unitExists = (p) => existsSync29(p);
14914
15157
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
14915
15158
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
14916
15159
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -14927,7 +15170,7 @@ if (command === "passenger-sync-daemon") {
14927
15170
  for (const a of plan.actions) {
14928
15171
  if (a.kind === "enable") {
14929
15172
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
14930
- writeFileSync15(cfg.configFile, planEnable(readFileSync21(cfg.configFile, "utf8"), a.route));
15173
+ writeFileSync16(cfg.configFile, planEnable(readFileSync22(cfg.configFile, "utf8"), a.route));
14931
15174
  enabledSound.add(a.route);
14932
15175
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
14933
15176
  } else {
@@ -14944,7 +15187,10 @@ ${changed2.map((c) => ` - ${c}
14944
15187
  `).join("")}` : "fix: nothing changed\n");
14945
15188
  }
14946
15189
  async function resolvedRepos(c) {
14947
- 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
+ {
14948
15194
  const id = repoIdForName(c, t.name);
14949
15195
  const o = c.repoOverrides[t.name];
14950
15196
  return {
@@ -14968,7 +15214,7 @@ ${changed2.map((c) => ` - ${c}
14968
15214
  worktrees: { prefix: shipWorktreePrefixFor(c) ?? await areaWorktreePrefix(c, id), nameOverride: o?.worktreeName }
14969
15215
  }, t.dir)
14970
15216
  };
14971
- }));
15217
+ }
14972
15218
  }
14973
15219
  const worktreeLocations = async (c) => (await resolvedRepos(c)).map((r) => ({
14974
15220
  parent: resolvePath(r.dir, ".."),
@@ -15923,7 +16169,7 @@ ${changed2.map((c) => ` - ${c}
15923
16169
  process.exit(1);
15924
16170
  }
15925
16171
  requireArmed("run the daemon loop");
15926
- const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join28(CREW_HOME, "src"), join28(CREW_HOME, "dist"), join28(CREW_HOME, "bin")];
16172
+ const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join29(CREW_HOME, "src"), join29(CREW_HOME, "dist"), join29(CREW_HOME, "bin")];
15927
16173
  const configPaths = daemonConfigPaths(cfg);
15928
16174
  watchPaths.push(...configPaths);
15929
16175
  noteDaemonStarted(cfg.ship.stateDir);
@@ -16050,69 +16296,27 @@ ${changed2.map((c) => ` - ${c}
16050
16296
  `);
16051
16297
  process.exit(2);
16052
16298
  }
16053
- const keyRole = target.resolved.role ?? await fetchKeyRole(target, cfg.ship.userAgent);
16054
- let readOnly = !isAdminRole(keyRole);
16055
- let personaResult;
16056
- const runPersonas = (ro) => syncPersonas(target, { userAgent: cfg.ship.userAgent, dryRun, force, readOnly: ro });
16299
+ let run2;
16057
16300
  try {
16058
- try {
16059
- personaResult = await runPersonas(readOnly);
16060
- } catch (e) {
16061
- if (readOnly || !isForbidden(e)) throw e;
16062
- readOnly = true;
16063
- personaResult = await runPersonas(true);
16064
- }
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
+ });
16065
16311
  } catch (e) {
16066
- if (e instanceof AgentsSyncError) {
16312
+ if (e instanceof AgentsSyncError || e instanceof SkillSyncError) {
16067
16313
  process.stderr.write(`crew agents sync: ${e.message}
16068
16314
  `);
16069
16315
  process.exit(2);
16070
16316
  }
16071
16317
  throw e;
16072
16318
  }
16073
- process.stdout.write(`route: ${target.route}
16074
- `);
16075
- for (const o of personaResult.outcomes) process.stdout.write(`${describeSyncOutcome(o)}
16076
- `);
16077
- for (const o of personaResult.crewLinks) process.stdout.write(`${describeCrewLink(o)}
16078
- `);
16079
- if (personaResult.releaseNote) process.stdout.write(`${personaResult.releaseNote}
16080
- `);
16081
- const briefs = Object.fromEntries(ROLE_NAMES.map((r) => [r, personaDefaultPrompt(target.promptsDir, r)]));
16082
- for (const w of skillMarkerWarnings(target.promptsDir, briefs)) process.stderr.write(`crew agents sync: warning: ${w}
16083
- `);
16084
- let skillResult;
16085
- if (!readOnly) {
16086
- try {
16087
- skillResult = await syncSkills(target, { userAgent: cfg.ship.userAgent, dryRun, force });
16088
- } catch (e) {
16089
- if (isForbidden(e)) readOnly = true;
16090
- else if (e instanceof SkillSyncError) {
16091
- process.stderr.write(`crew agents sync: ${e.message}
16092
- `);
16093
- process.exit(2);
16094
- } else throw e;
16095
- }
16096
- }
16097
- for (const o of skillResult?.outcomes ?? []) process.stdout.write(`${describeSkillSyncOutcome(o)}
16098
- `);
16099
- if (!dryRun) {
16100
- const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16101
- const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
16102
- raw.agentPersonas = personaResult.agentPersonas;
16103
- if (skillResult) raw.agentSkills = skillResult.agentSkills;
16104
- if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
16105
- mkdirSync14(dirOf(resolvedPath), { recursive: true });
16106
- writeFileSync15(resolvedPath, `${JSON.stringify(raw, null, 2)}
16107
- `);
16108
- }
16109
- if (!dryRun) {
16110
- target.resolved.agentPersonas = personaResult.agentPersonas;
16111
- if (personaResult.releaseAgentId) target.resolved.releaseAgentId = personaResult.releaseAgentId;
16112
- }
16113
- if (readOnly) {
16114
- 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");
16115
- }
16319
+ const { personaResult, skillResult } = run2;
16116
16320
  if (flag("revoke-seat-keys")) {
16117
16321
  for (const o of await revokeSeatKeys(target, { userAgent: cfg.ship.userAgent, dryRun })) {
16118
16322
  process.stdout.write(`${describeRevokeOutcome(o)}
@@ -16124,9 +16328,7 @@ ${changed2.map((c) => ` - ${c}
16124
16328
  `);
16125
16329
  }
16126
16330
  }
16127
- if (personaResult.outcomes.some((o) => o.action === "diverged") || (skillResult?.outcomes.some((o) => o.action === "diverged") ?? false)) {
16128
- process.exitCode = 1;
16129
- }
16331
+ if (run2.diverged) process.exitCode = 1;
16130
16332
  break;
16131
16333
  }
16132
16334
  case "skills": {
@@ -16168,10 +16370,10 @@ ${changed2.map((c) => ` - ${c}
16168
16370
  `);
16169
16371
  if (!dryRun) {
16170
16372
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
16171
- const raw = existsSync28(resolvedPath) ? JSON.parse(readFileSync21(resolvedPath, "utf8")) : {};
16373
+ const raw = existsSync29(resolvedPath) ? JSON.parse(readFileSync22(resolvedPath, "utf8")) : {};
16172
16374
  raw.agentSkills = result.agentSkills;
16173
- mkdirSync14(dirOf(resolvedPath), { recursive: true });
16174
- writeFileSync15(resolvedPath, `${JSON.stringify(raw, null, 2)}
16375
+ mkdirSync15(dirOf(resolvedPath), { recursive: true });
16376
+ writeFileSync16(resolvedPath, `${JSON.stringify(raw, null, 2)}
16175
16377
  `);
16176
16378
  }
16177
16379
  if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
@@ -16184,14 +16386,14 @@ ${changed2.map((c) => ` - ${c}
16184
16386
  process.exit(2);
16185
16387
  };
16186
16388
  if (sub !== "add" && sub !== "list" && sub !== "clone") usage2();
16187
- const configText = readFileSync21(cfg.configFile, "utf8");
16389
+ const configText = readFileSync22(cfg.configFile, "utf8");
16188
16390
  let routeName = positional[2];
16189
16391
  let addPath = ".";
16190
16392
  let addName;
16191
16393
  if (sub === "list" && !routeName) usage2();
16192
16394
  if (sub === "add") {
16193
16395
  try {
16194
- 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()))));
16195
16397
  routeName = a.route;
16196
16398
  addPath = a.path;
16197
16399
  addName = a.name;
@@ -16220,15 +16422,15 @@ ${changed2.map((c) => ` - ${c}
16220
16422
  }
16221
16423
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
16222
16424
  let resolvedRemotes;
16223
- if (existsSync28(resolvedFile)) {
16425
+ if (existsSync29(resolvedFile)) {
16224
16426
  try {
16225
- resolvedRemotes = JSON.parse(readFileSync21(resolvedFile, "utf8"));
16427
+ resolvedRemotes = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16226
16428
  } catch {
16227
16429
  }
16228
16430
  }
16229
16431
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
16230
16432
  const text = configText;
16231
- const base = dirname14(cfg.configFile);
16433
+ const base = dirname16(cfg.configFile);
16232
16434
  try {
16233
16435
  if (sub === "list") {
16234
16436
  const repos = configuredRepos(text, routeName, base);
@@ -16237,7 +16439,7 @@ ${changed2.map((c) => ` - ${c}
16237
16439
  for (const r of repos) {
16238
16440
  const origin2 = originOf(r.dir);
16239
16441
  const row = matchRepoName(origin2, resolvedRemotes);
16240
- 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)"}`;
16241
16443
  process.stdout.write(`${r.name} ${r.dir} ${note}
16242
16444
  `);
16243
16445
  }
@@ -16249,9 +16451,9 @@ ${changed2.map((c) => ` - ${c}
16249
16451
  ` : `${human}
16250
16452
  `);
16251
16453
  const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
16252
- 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)) {
16253
16455
  throw new ReposError(
16254
- 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`
16255
16457
  );
16256
16458
  }
16257
16459
  if (dryRun) {
@@ -16269,12 +16471,12 @@ ${changed2.map((c) => ` - ${c}
16269
16471
  throw new ReposError(`${e.message}${authHint}
16270
16472
  crew.yaml was not changed`);
16271
16473
  }
16272
- writeFileSync15(cfg.configFile, plan2.text);
16474
+ writeFileSync16(cfg.configFile, plan2.text);
16273
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}`);
16274
16476
  break;
16275
16477
  }
16276
16478
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
16277
- const top = existsSync28(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16479
+ const top = existsSync29(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
16278
16480
  if (!top) throw new ReposError(`${target} is not a git checkout`);
16279
16481
  const origin = originOf(top);
16280
16482
  const plan = planRepoAdd({
@@ -16323,15 +16525,15 @@ ${changed2.map((c) => ` - ${c}
16323
16525
  const created = await rowTracker.createRepoRow(rowPayload);
16324
16526
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
16325
16527
  `);
16326
- if (existsSync28(resolvedFile)) {
16327
- const raw = JSON.parse(readFileSync21(resolvedFile, "utf8"));
16528
+ if (existsSync29(resolvedFile)) {
16529
+ const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
16328
16530
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
16329
16531
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
16330
- writeFileSync15(resolvedFile, `${JSON.stringify(raw, null, 2)}
16532
+ writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
16331
16533
  `);
16332
16534
  }
16333
16535
  }
16334
- writeFileSync15(cfg.configFile, plan.text);
16536
+ writeFileSync16(cfg.configFile, plan.text);
16335
16537
  process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
16336
16538
  `);
16337
16539
  } catch (e) {
@@ -16641,10 +16843,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16641
16843
  }
16642
16844
  }
16643
16845
  let previous;
16644
- const hadPreviousFile = existsSync28(resolvedPath);
16846
+ const hadPreviousFile = existsSync29(resolvedPath);
16645
16847
  if (hadPreviousFile) {
16646
16848
  try {
16647
- previous = JSON.parse(readFileSync21(resolvedPath, "utf8"));
16849
+ previous = JSON.parse(readFileSync22(resolvedPath, "utf8"));
16648
16850
  } catch {
16649
16851
  }
16650
16852
  }
@@ -16752,10 +16954,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16752
16954
  process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
16753
16955
  `);
16754
16956
  } else {
16755
- mkdirSync14(dirname14(resolvedPath), { recursive: true });
16957
+ mkdirSync15(dirname16(resolvedPath), { recursive: true });
16756
16958
  if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
16757
16959
  const tmpPath = `${resolvedPath}.tmp${process.pid}`;
16758
- writeFileSync15(tmpPath, `${JSON.stringify(resolved, null, 2)}
16960
+ writeFileSync16(tmpPath, `${JSON.stringify(resolved, null, 2)}
16759
16961
  `);
16760
16962
  renameSync2(tmpPath, resolvedPath);
16761
16963
  process.stderr.write(`Resolved ids written to ${resolvedPath}
@@ -16790,8 +16992,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16790
16992
  process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
16791
16993
  `);
16792
16994
  } else {
16793
- mkdirSync14(dirname14(apiKeyFile), { recursive: true });
16794
- writeFileSync15(apiKeyFile, `${apiKeyVar}=${apiKey}
16995
+ mkdirSync15(dirname16(apiKeyFile), { recursive: true });
16996
+ writeFileSync16(apiKeyFile, `${apiKeyVar}=${apiKey}
16795
16997
  `, { mode: 384 });
16796
16998
  process.stderr.write(`API key written to ${apiKeyFile}
16797
16999
  `);
@@ -16800,7 +17002,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16800
17002
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
16801
17003
  const wizard = firstRun;
16802
17004
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
16803
- const routeDir = value("dir") ?? (writeFirstRun ? join28(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
17005
+ const routeDir = value("dir") ?? (writeFirstRun ? join29(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
16804
17006
  const routeBlock = renderConnection(
16805
17007
  found,
16806
17008
  route2,
@@ -16892,6 +17094,56 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16892
17094
  `
16893
17095
  );
16894
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
+ }
16895
17147
  connectEvents.emit("done", `connected ${route2}`, {
16896
17148
  route: route2,
16897
17149
  resolvedPath: dryRun ? void 0 : resolvedPath,
@@ -17370,8 +17622,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17370
17622
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
17371
17623
  break;
17372
17624
  }
17373
- mkdirSync14(cfg.ship.stateDir, { recursive: true });
17374
- writeFileSync15(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
17625
+ mkdirSync15(cfg.ship.stateDir, { recursive: true });
17626
+ writeFileSync16(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
17375
17627
  `);
17376
17628
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
17377
17629
  if (flag("now") && control?.stopCommand && control.startCommand) {
@@ -17422,7 +17674,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17422
17674
  }
17423
17675
  case "log": {
17424
17676
  try {
17425
- const text = readFileSync21(cfg.ship.logFile, "utf8").split("\n");
17677
+ const text = readFileSync22(cfg.ship.logFile, "utf8").split("\n");
17426
17678
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
17427
17679
  `);
17428
17680
  } catch {
@@ -17431,6 +17683,121 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17431
17683
  }
17432
17684
  break;
17433
17685
  }
17686
+ case "sample-ticket": {
17687
+ const json = flag("json");
17688
+ const say = (event, extra, human) => process.stdout.write(json ? `${JSON.stringify({ event, ...extra })}
17689
+ ` : `${human}
17690
+ `);
17691
+ const fail = (msg) => {
17692
+ if (json) process.stdout.write(`${JSON.stringify({ event: "error", message: msg })}
17693
+ `);
17694
+ else process.stderr.write(`crew sample-ticket: ${msg}
17695
+ `);
17696
+ return process.exit(2);
17697
+ };
17698
+ if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
17699
+ const tracker = new Tracker(route, cfg.ship);
17700
+ const configText = readFileSync22(cfg.configFile, "utf8");
17701
+ const cfgBase = dirname16(cfg.configFile);
17702
+ const configured = configuredRepos(configText, route.route, cfgBase);
17703
+ const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
17704
+ const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
17705
+ const rows = await tracker.repoRows();
17706
+ const row = rows.find((r) => r.name === SAMPLE_REPO);
17707
+ if (flag("remove")) {
17708
+ const closed = [];
17709
+ if (row) {
17710
+ const wontFix = tracker.contract.statuses.wontFix;
17711
+ const resolved = new Set(tracker.contract.statuses.resolved);
17712
+ for (const t of await tracker.ticketsForRepo(row.id)) {
17713
+ if (resolved.has(t.status) || !isSampleTicket(t.description)) continue;
17714
+ if (!dryRun) {
17715
+ await tracker.updateTicket(t.id, {
17716
+ [tracker.contract.columns.status]: wontFix,
17717
+ [tracker.contract.columns.resolutionNote]: "Sample ticket removed by `crew sample-ticket --remove`; obsolete."
17718
+ });
17719
+ }
17720
+ closed.push(t.issue_tag ?? t.issue_id);
17721
+ }
17722
+ }
17723
+ if (dryRun) {
17724
+ say("dry-run", { dir, row: row?.id ?? null, closed }, `would close ${closed.join(", ") || "no tickets"}, delete the Repos row, detach and delete ${dir}`);
17725
+ break;
17726
+ }
17727
+ if (configured.some((r) => r.dir === dir)) {
17728
+ try {
17729
+ writeFileSync16(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
17730
+ } catch (e) {
17731
+ if (e instanceof ReposError) fail(e.message);
17732
+ throw e;
17733
+ }
17734
+ }
17735
+ if (row) {
17736
+ await tracker.deleteRepoRow(row.id);
17737
+ if (existsSync29(resolvedFile)) {
17738
+ const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
17739
+ if (raw.repoNames) delete raw.repoNames[row.id];
17740
+ if (raw.repoRemotes) delete raw.repoRemotes[row.id];
17741
+ writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
17742
+ `);
17743
+ }
17744
+ }
17745
+ if (existsSync29(join29(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync11(dir, { recursive: true, force: true });
17746
+ say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
17747
+ break;
17748
+ }
17749
+ const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
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`);
17752
+ if (dryRun) {
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`);
17754
+ break;
17755
+ }
17756
+ if (fresh) {
17757
+ mkdirSync15(dir, { recursive: true });
17758
+ writeFileSync16(join29(dir, "README.md"), SAMPLE_README);
17759
+ writeFileSync16(join29(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
17760
+ const g = (...a) => {
17761
+ const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
17762
+ if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
17763
+ };
17764
+ g("init", "-b", "main");
17765
+ g("add", ".");
17766
+ g("-c", "user.name=crew", "-c", "user.email=crew@localhost", "commit", "-m", "Initial commit");
17767
+ }
17768
+ let repoId = row?.id;
17769
+ if (!repoId) {
17770
+ const payload = planRepoRow({
17771
+ name: SAMPLE_REPO,
17772
+ origin: SAMPLE_REMOTE,
17773
+ projectId,
17774
+ spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
17775
+ });
17776
+ repoId = (await tracker.createRepoRow(payload)).id;
17777
+ if (existsSync29(resolvedFile)) {
17778
+ const raw = JSON.parse(readFileSync22(resolvedFile, "utf8"));
17779
+ raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
17780
+ raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
17781
+ writeFileSync16(resolvedFile, `${JSON.stringify(raw, null, 2)}
17782
+ `);
17783
+ }
17784
+ }
17785
+ if (!configured.some((r) => r.dir === dir)) {
17786
+ try {
17787
+ writeFileSync16(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
17788
+ } catch (e) {
17789
+ if (e instanceof ReposError) fail(e.message);
17790
+ throw e;
17791
+ }
17792
+ }
17793
+ const ticket = await tracker.fileTicket(
17794
+ planSampleTicket({ projectId, repoId, approvedStatus: tracker.contract.statuses.approved }),
17795
+ route.resolved.seats.dev ?? ""
17796
+ );
17797
+ const tag = ticket.issue_tag ?? ticket.issue_id;
17798
+ say("filed", { ticket: tag, id: ticket.id, dir, repoId }, `filed ${tag} against ${dir}; follow it with \`crew watch\``);
17799
+ break;
17800
+ }
17434
17801
  case "selftest": {
17435
17802
  const wanted = value("toolchain");
17436
17803
  const all = shipToolchains(cfg.ship);
@@ -17609,7 +17976,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
17609
17976
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
17610
17977
  // config-provenance display; this is the field that actually
17611
17978
  // describes where a worktree lands by default.
17612
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename9(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
17979
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename10(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
17613
17980
  worktreeParent: resolvePath(r.dir, "..")
17614
17981
  })),
17615
17982
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -17694,6 +18061,40 @@ watermark: ${state.watermark()}
17694
18061
  }
17695
18062
  case "doctor": {
17696
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
+ }
17697
18098
  process.stdout.write(
17698
18099
  `ship: ${cfg.ship.name} (${host})
17699
18100
  routes: ${cfg.routes.map((c) => c.route).join(", ")}
@@ -17701,7 +18102,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
17701
18102
  );
17702
18103
  try {
17703
18104
  const evFile = eventFileFor(cfg.ship.stateDir);
17704
- const evSize = existsSync28(evFile) ? statSync9(evFile).size : 0;
18105
+ const evSize = existsSync29(evFile) ? statSync9(evFile).size : 0;
17705
18106
  process.stdout.write(
17706
18107
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
17707
18108
  ` : "events log: ok\n"
@@ -17712,7 +18113,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
17712
18113
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
17713
18114
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
17714
18115
  try {
17715
- return readFileSync21(p, "utf8");
18116
+ return readFileSync22(p, "utf8");
17716
18117
  } catch {
17717
18118
  return null;
17718
18119
  }
@@ -17742,7 +18143,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
17742
18143
  );
17743
18144
  for (const r of await resolvedRepos(route)) {
17744
18145
  const { config } = r;
17745
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename9(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
18146
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename10(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
17746
18147
  process.stdout.write(
17747
18148
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
17748
18149
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
@@ -17917,6 +18318,8 @@ crew manifest: ${seats2.length ? seats2.map((c) => c.name).join(", ") : "non
17917
18318
  if (ov.toolchain) process.stdout.write(`repo toolchain: ${repoName} \u2192 ${ov.toolchain} (binary and MCP checked above, as for every ship toolchain)
17918
18319
  `);
17919
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));
17920
18323
  process.stdout.write(`git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler PATH"}
17921
18324
  `);
17922
18325
  if (!allBinsFound || !gitPath) {