@tablation/crew 0.50.0 → 0.50.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +211 -112
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,5 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/doctor-repo.ts
4
+ import { existsSync } from "fs";
5
+ function doctorBaseReport(r, base, health) {
6
+ const pad = " ";
7
+ if (!existsSync(r.dir)) {
8
+ return {
9
+ text: `${pad}- ${r.dir} is missing on disk \u2014 no .crew.yaml read, settings above are defaults
10
+ `,
11
+ failed: true,
12
+ skipRest: true
13
+ };
14
+ }
15
+ try {
16
+ const bh = health(r);
17
+ if (bh.action === "not-applicable") return { text: "", failed: false, skipRest: false };
18
+ return {
19
+ text: `${pad}base ${base}: ${bh.unsafe ? "ERROR \u2014 " : ""}${bh.detail}
20
+ `,
21
+ failed: bh.unsafe,
22
+ skipRest: false
23
+ };
24
+ } catch (e) {
25
+ return {
26
+ text: `${pad}base ${base}: could not check \u2014 ${e instanceof Error ? e.message : String(e)}
27
+ `,
28
+ failed: true,
29
+ skipRest: false
30
+ };
31
+ }
32
+ }
33
+
3
34
  // src/cli.ts
4
35
  import { resolve as resolve4, dirname as dirname12, join as join25, basename as basename9 } from "path";
5
36
  import { fileURLToPath } from "url";
@@ -62,7 +93,7 @@ function pickWorktreeRoute(ticketNumber, candidates) {
62
93
  }
63
94
 
64
95
  // src/config.ts
65
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
96
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
66
97
  import { resolve, dirname as dirname3, isAbsolute, join as join7, basename as basename3 } from "path";
67
98
  import { homedir as homedir5, tmpdir } from "os";
68
99
  import { parse as parse2 } from "yaml";
@@ -71,7 +102,7 @@ import { parse as parse2 } from "yaml";
71
102
  import {
72
103
  appendFileSync,
73
104
  mkdirSync,
74
- existsSync,
105
+ existsSync as existsSync2,
75
106
  statSync,
76
107
  renameSync,
77
108
  openSync,
@@ -90,7 +121,7 @@ var ROTATED_EVENTS_RE = /^events\.\d{8}T\d{6}Z(?:-\d+)?\.jsonl$/;
90
121
  var ROTATE_LOCK_STALE_MS = 6e4;
91
122
  function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
92
123
  try {
93
- if (!existsSync(file) || statSync(file).size <= maxBytes) return void 0;
124
+ if (!existsSync2(file) || statSync(file).size <= maxBytes) return void 0;
94
125
  } catch {
95
126
  return void 0;
96
127
  }
@@ -111,7 +142,7 @@ function rotateEventsFile(file, maxBytes, now = /* @__PURE__ */ new Date()) {
111
142
  if (statSync(file).size <= maxBytes) return void 0;
112
143
  const stamp = now.toISOString().replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
113
144
  let dest = join(dirname(file), `events.${stamp}.jsonl`);
114
- for (let n = 1; existsSync(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
145
+ for (let n = 1; existsSync2(dest); n++) dest = join(dirname(file), `events.${stamp}-${n}.jsonl`);
115
146
  renameSync(file, dest);
116
147
  return dest;
117
148
  } finally {
@@ -213,7 +244,7 @@ var Emitter = class _Emitter {
213
244
  var eventFileFor = (stateDir) => join(stateDir, "events.jsonl");
214
245
 
215
246
  // src/pair-install.ts
216
- import { existsSync as existsSync4, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
247
+ import { existsSync as existsSync5, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "fs";
217
248
  import { spawnSync as spawnSync3 } from "child_process";
218
249
  import { homedir as homedir4 } from "os";
219
250
  import { join as join5 } from "path";
@@ -240,7 +271,7 @@ function mapStreamLine(line) {
240
271
  if (block.type === "thinking") out.push({ kind: "thought", text: block.thinking });
241
272
  else if (block.type === "text") out.push({ kind: "text", text: block.text });
242
273
  else if (block.type === "tool_use") {
243
- out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input) });
274
+ out.push({ kind: "tool", tool: block.name, target: toolTarget(block.name, block.input), input: block.input });
244
275
  }
245
276
  }
246
277
  return out;
@@ -342,7 +373,7 @@ function removeClaudeMcp(o) {
342
373
 
343
374
  // src/launcher.ts
344
375
  import { basename, join as join3 } from "path";
345
- import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync } from "fs";
376
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
346
377
  import { homedir as homedir2 } from "os";
347
378
  function isOriLauncher(spec) {
348
379
  return !!spec.launcher && basename(spec.launcher) === "ori";
@@ -405,7 +436,7 @@ function omlxModelPresent(model, dirs) {
405
436
  }
406
437
  function readOmlxSettings(home = homedir2()) {
407
438
  const p = join3(home, ".omlx", "settings.json");
408
- return existsSync2(p) ? parseOmlxSettings(readFileSync3(p, "utf8")) : void 0;
439
+ return existsSync3(p) ? parseOmlxSettings(readFileSync3(p, "utf8")) : void 0;
409
440
  }
410
441
 
411
442
  // src/capability.ts
@@ -542,7 +573,7 @@ import { join as join4 } from "path";
542
573
  import { rmSync as rmSync3, writeFileSync as writeFileSync2 } from "fs";
543
574
 
544
575
  // src/pair-files.ts
545
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync, rmSync as rmSync2 } from "fs";
576
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync, rmSync as rmSync2 } from "fs";
546
577
  import { dirname as dirname2 } from "path";
547
578
  var PAIR_BLOCK_START = "<!-- crew:pair start -->";
548
579
  var PAIR_BLOCK_END = "<!-- crew:pair end -->";
@@ -583,7 +614,7 @@ function removePairBlock(text) {
583
614
  return text.replace(new RegExp(`\\n*${BLOCK_RE.source}`), "\n").replace(/^\n+/, "").replace(/\n{3,}/g, "\n\n");
584
615
  }
585
616
  function readIfExists(path) {
586
- return existsSync3(path) ? readFileSync4(path, "utf8") : void 0;
617
+ return existsSync4(path) ? readFileSync4(path, "utf8") : void 0;
587
618
  }
588
619
  function writeFileMkdir(path, text) {
589
620
  mkdirSync2(dirname2(path), { recursive: true });
@@ -980,7 +1011,7 @@ function vscodeUserDirs(o) {
980
1011
  const platform2 = o.platform ?? process.platform;
981
1012
  const root = platform2 === "darwin" ? join5(home, "Library", "Application Support") : platform2 === "win32" ? process.env.APPDATA ?? join5(home, "AppData", "Roaming") : join5(home, ".config");
982
1013
  const all = VSCODE_FLAVOURS.map((f) => join5(root, f, "User"));
983
- const present = all.filter((d) => existsSync4(d));
1014
+ const present = all.filter((d) => existsSync5(d));
984
1015
  return present.length > 0 ? present : [all[0]];
985
1016
  }
986
1017
  var COPILOT_FRONTMATTER = '---\napplyTo: "**"\n---\n\n';
@@ -1372,7 +1403,7 @@ function explain(ship, required) {
1372
1403
  }
1373
1404
 
1374
1405
  // src/repo-config.ts
1375
- import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
1406
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
1376
1407
  import { join as join6, basename as basename2 } from "path";
1377
1408
  import { parse } from "yaml";
1378
1409
  var SPEC_VERSION = 1;
@@ -1456,7 +1487,7 @@ var PROVIDERS = ["github", "buildkite", "other", "none"];
1456
1487
  function findRepoConfig(dir) {
1457
1488
  for (const name of REPO_CONFIG_FILENAMES) {
1458
1489
  const p = join6(dir, name);
1459
- if (existsSync5(p)) return p;
1490
+ if (existsSync6(p)) return p;
1460
1491
  }
1461
1492
  return null;
1462
1493
  }
@@ -2181,7 +2212,7 @@ function configSearchPath(crewHome) {
2181
2212
  }
2182
2213
  function findConfigFile(crewHome) {
2183
2214
  const candidates = configSearchPath(crewHome);
2184
- return candidates.find((c) => existsSync6(c)) ?? candidates[candidates.length - 1];
2215
+ return candidates.find((c) => existsSync7(c)) ?? candidates[candidates.length - 1];
2185
2216
  }
2186
2217
  var defaultConfigPath = () => join7(process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config"), "crew", "crew.yaml");
2187
2218
  function routeSlug(route) {
@@ -2204,7 +2235,7 @@ function splitRoute(route) {
2204
2235
  }
2205
2236
  function readResolvedFile(stateDir, route) {
2206
2237
  const path = resolvedPathFor(stateDir, route);
2207
- if (!existsSync6(path)) return void 0;
2238
+ if (!existsSync7(path)) return void 0;
2208
2239
  try {
2209
2240
  return JSON.parse(readFileSync6(path, "utf8"));
2210
2241
  } catch (e) {
@@ -2263,7 +2294,7 @@ function parseResolved(raw, m, where) {
2263
2294
  }
2264
2295
  function loadConfig(crewHome, configFile, opts = {}) {
2265
2296
  const file = configFile ?? findConfigFile(crewHome);
2266
- if (opts.text === void 0 && !existsSync6(file)) {
2297
+ if (opts.text === void 0 && !existsSync7(file)) {
2267
2298
  throw new ConfigError(
2268
2299
  `no config found. Looked in:
2269
2300
  ` + configSearchPath(crewHome).map((c) => ` - ${c}`).join("\n") + `
@@ -2573,7 +2604,7 @@ function repoIdForName(r, name) {
2573
2604
  }
2574
2605
  function resolveApiKey(r) {
2575
2606
  if (r.apiKey) return r.apiKey;
2576
- if (r.apiKeyFile && r.apiKeyVar && existsSync6(r.apiKeyFile)) {
2607
+ if (r.apiKeyFile && r.apiKeyVar && existsSync7(r.apiKeyFile)) {
2577
2608
  for (const line of readFileSync6(r.apiKeyFile, "utf8").split("\n")) {
2578
2609
  const m = line.match(new RegExp(`^${r.apiKeyVar}=(.*)$`));
2579
2610
  if (m) return (m[1] ?? "").trim().replace(/^["']|["']$/g, "");
@@ -2793,7 +2824,7 @@ function rosterMarkdown(roster, meId) {
2793
2824
  }
2794
2825
 
2795
2826
  // src/state.ts
2796
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
2827
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync8, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "fs";
2797
2828
  import { join as join8 } from "path";
2798
2829
  var EPOCH = "1970-01-01T00:00:00.000Z";
2799
2830
  var State = class _State {
@@ -2839,10 +2870,10 @@ var State = class _State {
2839
2870
  }
2840
2871
  /** Whole-crew pause, and the per-role sentinels one level down. */
2841
2872
  isPaused() {
2842
- return existsSync7(this.path(".crew-paused"));
2873
+ return existsSync8(this.path(".crew-paused"));
2843
2874
  }
2844
2875
  isRolePaused(role) {
2845
- return existsSync7(this.path(`.role-paused-${role}`));
2876
+ return existsSync8(this.path(`.role-paused-${role}`));
2846
2877
  }
2847
2878
  pausedRoles(roles) {
2848
2879
  return new Set(roles.filter((r) => this.isRolePaused(r)));
@@ -2859,7 +2890,7 @@ var State = class _State {
2859
2890
  * while agents keep working.
2860
2891
  */
2861
2892
  isReleasePaused() {
2862
- return existsSync7(this.path(".release-paused"));
2893
+ return existsSync8(this.path(".release-paused"));
2863
2894
  }
2864
2895
  setReleasePaused(paused) {
2865
2896
  if (paused) writeFileSync4(this.path(".release-paused"), "");
@@ -3507,7 +3538,7 @@ function describeRelease(o, route) {
3507
3538
  // src/install.ts
3508
3539
  import { dirname as dirname4, join as join9, extname } from "path";
3509
3540
  import { homedir as homedir6 } from "os";
3510
- import { accessSync, constants, existsSync as existsSync8, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
3541
+ import { accessSync, constants, existsSync as existsSync9, mkdirSync as mkdirSync4, readdirSync as readdirSync2, readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync5, unlinkSync, realpathSync } from "fs";
3511
3542
  import { spawnSync as spawnSync4 } from "child_process";
3512
3543
  import { createHash } from "crypto";
3513
3544
 
@@ -3882,7 +3913,7 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
3882
3913
  }
3883
3914
  }
3884
3915
  for (const path of plan.unitPaths) {
3885
- if (!existsSync8(path)) continue;
3916
+ if (!existsSync9(path)) continue;
3886
3917
  if (dryRun) {
3887
3918
  log.emit(`would remove ${path}`);
3888
3919
  continue;
@@ -3918,7 +3949,7 @@ function loadedCrewLabels() {
3918
3949
  return [...parseLaunchctlList(list.stdout ?? "").keys()].filter((l) => l.startsWith(CREW_LAUNCHD_PREFIX));
3919
3950
  }
3920
3951
  function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3921
- const files = existsSync8(dir) ? readdirSync2(dir) : [];
3952
+ const files = existsSync9(dir) ? readdirSync2(dir) : [];
3922
3953
  const units = files.filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
3923
3954
  const plistPath = join9(dir, f);
3924
3955
  let program = null;
@@ -3930,7 +3961,7 @@ function listLaunchdUnits(dir = launchAgentsDir(), loaded = []) {
3930
3961
  label: f.slice(0, -".plist".length),
3931
3962
  plistPath,
3932
3963
  program,
3933
- programExists: program !== null && existsSync8(program)
3964
+ programExists: program !== null && existsSync9(program)
3934
3965
  };
3935
3966
  });
3936
3967
  const seen = new Set(units.map((u) => u.label));
@@ -3949,9 +3980,9 @@ function removeLaunchdUnit(unit, dryRun, log) {
3949
3980
  log.emit(`would unload and remove ${unit.label}`);
3950
3981
  return;
3951
3982
  }
3952
- if (existsSync8(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3983
+ if (existsSync9(unit.plistPath)) spawnSync4("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
3953
3984
  spawnSync4("launchctl", ["remove", unit.label], { encoding: "utf8" });
3954
- if (existsSync8(unit.plistPath)) unlinkSync(unit.plistPath);
3985
+ if (existsSync9(unit.plistPath)) unlinkSync(unit.plistPath);
3955
3986
  log.emit(`removed ${unit.label} (${unit.plistPath})`);
3956
3987
  }
3957
3988
  function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir, loaded) {
@@ -4034,7 +4065,7 @@ import { statfsSync } from "fs";
4034
4065
  // src/git.ts
4035
4066
  import { execFileSync } from "child_process";
4036
4067
  import {
4037
- existsSync as existsSync9,
4068
+ existsSync as existsSync10,
4038
4069
  mkdirSync as mkdirSync5,
4039
4070
  openSync as openSync2,
4040
4071
  closeSync as closeSync2,
@@ -4434,7 +4465,7 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
4434
4465
  try {
4435
4466
  const st = statSync4(root);
4436
4467
  if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
4437
- if (existsSync9(join10(root, "checkout"))) continue;
4468
+ if (existsSync10(join10(root, "checkout"))) continue;
4438
4469
  rmSync6(root, { recursive: true, force: true });
4439
4470
  removed.push(name);
4440
4471
  } catch {
@@ -4477,7 +4508,7 @@ function originUrlOf(dir, remoteName) {
4477
4508
  }
4478
4509
  }
4479
4510
  function ensureRepoCheckout(dir, remote, opts = {}) {
4480
- if (existsSync9(`${dir}/.git`)) return false;
4511
+ if (existsSync10(`${dir}/.git`)) return false;
4481
4512
  if (!remote) {
4482
4513
  throw new GitError(
4483
4514
  `${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
@@ -4540,10 +4571,10 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
4540
4571
  function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
4541
4572
  const release = acquireCheckoutLock(dir);
4542
4573
  try {
4543
- if (!existsSync9(`${dir}/.git`) && githubRemote) {
4574
+ if (!existsSync10(`${dir}/.git`) && githubRemote) {
4544
4575
  const url = cloneUrlFor(githubRemote, clone);
4545
4576
  const marker = cloneFailureMarker(dir);
4546
- if (existsSync9(marker) && readFileSync9(marker, "utf8").split("\n")[0] === url) {
4577
+ if (existsSync10(marker) && readFileSync9(marker, "utf8").split("\n")[0] === url) {
4547
4578
  return { action: "clone-failed", detail: readFileSync9(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
4548
4579
  }
4549
4580
  try {
@@ -7126,7 +7157,7 @@ var alreadyReported = (ticket, g, events) => events.some((e) => e.record_id ===
7126
7157
 
7127
7158
  // src/agent.ts
7128
7159
  import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
7129
- import { readFileSync as readFileSync12, existsSync as existsSync12, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
7160
+ import { readFileSync as readFileSync12, existsSync as existsSync13, statSync as statSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8, createWriteStream, openSync as openSync3, closeSync as closeSync3, readSync } from "fs";
7130
7161
  import { StringDecoder } from "string_decoder";
7131
7162
  import { finished } from "stream/promises";
7132
7163
  import { join as join14, dirname as dirname6 } from "path";
@@ -7544,7 +7575,7 @@ function expandSkillMarkers(text, lookup, opts = {}) {
7544
7575
  }
7545
7576
 
7546
7577
  // src/skills.ts
7547
- import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync11 } from "fs";
7578
+ import { readFileSync as readFileSync11, readdirSync as readdirSync4, existsSync as existsSync12 } from "fs";
7548
7579
  import { join as join13 } from "path";
7549
7580
  import { parse as parseYaml } from "yaml";
7550
7581
  import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError8, TablationApiError as TablationApiError2 } from "@tablation/client";
@@ -7561,7 +7592,7 @@ ${additions}`;
7561
7592
  }
7562
7593
 
7563
7594
  // src/agents.ts
7564
- import { readFileSync as readFileSync10, existsSync as existsSync10 } from "fs";
7595
+ import { readFileSync as readFileSync10, existsSync as existsSync11 } from "fs";
7565
7596
  import { join as join12 } from "path";
7566
7597
  import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError7, TablationApiError } from "@tablation/client";
7567
7598
  var PERSONA_NAME = {
@@ -7577,10 +7608,10 @@ var AgentsSyncError = class extends Error {
7577
7608
  };
7578
7609
  function personaDefaultPrompt(promptsDir, role) {
7579
7610
  const briefPath = join12(promptsDir, "personas", `lane-${role}.md`);
7580
- if (!existsSync10(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7611
+ if (!existsSync11(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
7581
7612
  if (SELF_CONTAINED_ROLES.has(role)) return readFileSync10(briefPath, "utf8");
7582
7613
  const commonPath = join12(promptsDir, "personas", "common.md");
7583
- if (!existsSync10(commonPath)) return readFileSync10(briefPath, "utf8");
7614
+ if (!existsSync11(commonPath)) return readFileSync10(briefPath, "utf8");
7584
7615
  return `${readFileSync10(commonPath, "utf8")}${readFileSync10(briefPath, "utf8")}`;
7585
7616
  }
7586
7617
  async function resolveAgentId(route, role, opts) {
@@ -7969,7 +8000,7 @@ function parseSkillFile(path) {
7969
8000
  }
7970
8001
  function listLocalSkills(promptsDir) {
7971
8002
  const dir = join13(promptsDir, "skills");
7972
- if (!existsSync11(dir)) return [];
8003
+ if (!existsSync12(dir)) return [];
7973
8004
  return readdirSync4(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join13(dir, f)));
7974
8005
  }
7975
8006
  async function syncSkills(route, opts) {
@@ -8161,6 +8192,15 @@ function ticketMentions(text) {
8161
8192
  var TicketAttributionTracker = class {
8162
8193
  #tokensByTicket = /* @__PURE__ */ new Map();
8163
8194
  #current;
8195
+ /** The ticket key most recently named in the transcript, or undefined before any. */
8196
+ get current() {
8197
+ return this.#current;
8198
+ }
8199
+ /** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
8200
+ note(text) {
8201
+ const mentions = ticketMentions(text);
8202
+ if (mentions.length > 0) this.#current = mentions[mentions.length - 1];
8203
+ }
8164
8204
  /** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
8165
8205
  add(text, tokens) {
8166
8206
  const mentions = ticketMentions(text);
@@ -8186,6 +8226,11 @@ var TicketAttributionTracker = class {
8186
8226
  return best;
8187
8227
  }
8188
8228
  };
8229
+ function isClaimWrite(tool, input) {
8230
+ if (!tool || !/records_controller_update$/.test(tool)) return false;
8231
+ const body = input?.body;
8232
+ return typeof body === "object" && body !== null && body.status === "in_progress";
8233
+ }
8189
8234
 
8190
8235
  // src/agent.ts
8191
8236
  var DIGEST_MAX_AGE_SECONDS = 600;
@@ -8208,17 +8253,17 @@ var AgentSpawnError = class extends AgentError {
8208
8253
  };
8209
8254
  function planAgentRun(o) {
8210
8255
  const briefPath = join14(o.route.promptsDir, "personas", `lane-${o.role}.md`);
8211
- if (!existsSync12(briefPath)) {
8256
+ if (!existsSync13(briefPath)) {
8212
8257
  throw new AgentError(`no brief at ${briefPath} \u2014 refusing to run an unscoped session`);
8213
8258
  }
8214
8259
  const commonPath = join14(o.route.promptsDir, "personas", "common.md");
8215
- const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync12(commonPath);
8260
+ const usesCommon = !SELF_CONTAINED_ROLES.has(o.role) && existsSync13(commonPath);
8216
8261
  const now = o.now ?? (() => Date.now());
8217
8262
  const routeLabel = routeSlug(o.route.route);
8218
8263
  const digestPath2 = join14(o.stateDir, `digest-${routeLabel}-${o.role}.md`);
8219
8264
  let digest;
8220
8265
  let ageSeconds;
8221
- if (existsSync12(digestPath2)) {
8266
+ if (existsSync13(digestPath2)) {
8222
8267
  ageSeconds = Math.floor((now() - statSync5(digestPath2).mtimeMs) / 1e3);
8223
8268
  if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync12(digestPath2, "utf8");
8224
8269
  }
@@ -8415,6 +8460,13 @@ var RunObserver = class {
8415
8460
  /** Set once the stream carries the harness's final result (turn count or cost). */
8416
8461
  sawResult = false;
8417
8462
  muted = false;
8463
+ /**
8464
+ * The label stamped on events. Starts as the poll hint and switches to the
8465
+ * ticket the run actually claims (CREW-1776); `id.ticket` stays the hint.
8466
+ */
8467
+ liveTicket;
8468
+ /** Called after `liveTicket` changes, so the sidecar can be rewritten. */
8469
+ onClaim;
8418
8470
  buf = "";
8419
8471
  mapper;
8420
8472
  id;
@@ -8426,6 +8478,7 @@ var RunObserver = class {
8426
8478
  this.adapter = adapter;
8427
8479
  this.eventsSink = eventsSink;
8428
8480
  this.emitCycle = emitCycle;
8481
+ this.liveTicket = id.ticket ?? void 0;
8429
8482
  this.mapper = adapter.createStreamMapper?.() ?? { map: (p) => adapter.mapStreamLine(p), flush: () => [] };
8430
8483
  }
8431
8484
  feed(text) {
@@ -8466,7 +8519,29 @@ var RunObserver = class {
8466
8519
  consume(parsed, events) {
8467
8520
  const plan = this.id;
8468
8521
  let turnText = "";
8522
+ for (const ev of events) if ((ev.kind === "thought" || ev.kind === "text") && ev.text) this.attribution.note(ev.text);
8469
8523
  for (const ev of events) {
8524
+ if (ev.kind === "tool" && isClaimWrite(ev.tool, ev.input)) {
8525
+ const claimed = this.attribution.current;
8526
+ if (claimed && claimed !== this.liveTicket) {
8527
+ if (!this.muted) {
8528
+ this.eventsSink?.write(`${JSON.stringify({
8529
+ at: (/* @__PURE__ */ new Date()).toISOString(),
8530
+ cycle: this.emitCycle,
8531
+ route: plan.route,
8532
+ step: "agent",
8533
+ level: "info",
8534
+ message: `ticket: ${this.liveTicket ?? "none"} \u2192 ${claimed}`,
8535
+ role: plan.role,
8536
+ ticket: claimed,
8537
+ data: { kind: "ticket", from: this.liveTicket ?? null, to: claimed }
8538
+ })}
8539
+ `);
8540
+ }
8541
+ this.liveTicket = claimed;
8542
+ if (!this.muted) this.onClaim?.();
8543
+ }
8544
+ }
8470
8545
  const e = {
8471
8546
  at: (/* @__PURE__ */ new Date()).toISOString(),
8472
8547
  cycle: this.emitCycle,
@@ -8475,7 +8550,7 @@ var RunObserver = class {
8475
8550
  level: "info",
8476
8551
  message: ev.kind === "tool" ? `${ev.tool ?? "tool"}${ev.target ? ` ${ev.target}` : ""}` : ev.kind,
8477
8552
  role: plan.role,
8478
- ...plan.ticket ? { ticket: plan.ticket } : {},
8553
+ ...this.liveTicket ? { ticket: this.liveTicket } : {},
8479
8554
  data: { kind: ev.kind, ...ev.text ? { text: ev.text } : {}, ...ev.tool ? { tool: ev.tool } : {}, ...ev.target ? { target: ev.target } : {} }
8480
8555
  };
8481
8556
  if (!this.muted) this.eventsSink?.write(`${JSON.stringify(e)}
@@ -8510,25 +8585,39 @@ var RunObserver = class {
8510
8585
  if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
8511
8586
  }
8512
8587
  };
8513
- async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8514
- const attributedTicket = obs.attribution.winner(id.ticket ?? void 0);
8515
- const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
8516
- const { result } = obs;
8588
+ function writeRunMeta(id, startedAt, liveTicket, extra) {
8517
8589
  try {
8518
8590
  writeFileSync8(`${id.streamPath}.meta.json`, JSON.stringify({
8519
8591
  route: id.route,
8520
8592
  role: id.role,
8521
- ticket: id.ticket,
8522
- attributedTicket: attributedTicket ?? null,
8593
+ hint: id.ticket,
8594
+ ticket: liveTicket ?? id.ticket,
8523
8595
  startedAt,
8524
- exitCode: end.code,
8525
8596
  model: id.model,
8597
+ toolchain: id.toolchain,
8598
+ harness: id.harness,
8599
+ provider: id.provider,
8600
+ exitCode: null,
8601
+ running: true,
8602
+ ...extra
8603
+ }));
8604
+ } catch {
8605
+ }
8606
+ }
8607
+ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8608
+ const attributedTicket = obs.attribution.winner(obs.liveTicket);
8609
+ const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
8610
+ const { result } = obs;
8611
+ try {
8612
+ writeRunMeta(id, startedAt, obs.liveTicket, {
8613
+ attributedTicket: attributedTicket ?? null,
8614
+ running: void 0,
8615
+ exitCode: end.code,
8526
8616
  ...end.interrupted ? { interrupted: end.interrupted } : {},
8527
- ...identity,
8528
8617
  sessionId: result?.sessionId ?? null,
8529
8618
  numTurns: result?.numTurns ?? null,
8530
8619
  totalCostUsd: result?.costUsd ?? null
8531
- }));
8620
+ });
8532
8621
  } catch {
8533
8622
  }
8534
8623
  const ms2 = Date.now() - Date.parse(startedAt);
@@ -8616,6 +8705,9 @@ function spawnAgent(plan, emit) {
8616
8705
  }
8617
8706
  }
8618
8707
  const obs = new RunObserver(id, adapter, eventsSink, emit.cycle);
8708
+ const meta = { pid: child.pid ?? null };
8709
+ obs.onClaim = () => writeRunMeta(id, startedAt, obs.liveTicket, meta);
8710
+ if (child.pid !== void 0) writeRunMeta(id, startedAt, obs.liveTicket, meta);
8619
8711
  const tail = new FileTail(plan.streamPath);
8620
8712
  const timer = setInterval(() => obs.feed(tail.poll()), TAIL_POLL_MS);
8621
8713
  let spawnFailed = false;
@@ -8674,12 +8766,12 @@ async function reattachRun(streamPath, sc, emit, o = {}) {
8674
8766
  }
8675
8767
 
8676
8768
  // src/run-worktree.ts
8677
- import { existsSync as existsSync14 } from "fs";
8769
+ import { existsSync as existsSync15 } from "fs";
8678
8770
  import { dirname as dirname7, join as join16 } from "path";
8679
8771
 
8680
8772
  // src/ports.ts
8681
8773
  import { execFileSync as execFileSync2 } from "child_process";
8682
- import { existsSync as existsSync13 } from "fs";
8774
+ import { existsSync as existsSync14 } from "fs";
8683
8775
  import { join as join15 } from "path";
8684
8776
  var run = (cmd, args) => {
8685
8777
  try {
@@ -8725,7 +8817,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
8725
8817
  }
8726
8818
  function worktreeExists(parent, prefix, n) {
8727
8819
  for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
8728
- if (existsSync13(join15(parent, name, ".git"))) return true;
8820
+ if (existsSync14(join15(parent, name, ".git"))) return true;
8729
8821
  }
8730
8822
  return false;
8731
8823
  }
@@ -8801,7 +8893,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
8801
8893
  const existing = worktreeForNumber(dir, num);
8802
8894
  const located = locateBranchForTicket(dir, cfg, t, role);
8803
8895
  const baseRef = hasRemote ? `${remote}/${base}` : base;
8804
- if (existing && !existing.prunable && existsSync14(existing.path)) {
8896
+ if (existing && !existing.prunable && existsSync15(existing.path)) {
8805
8897
  const branch = existing.branch;
8806
8898
  if (status(existing.path).length > 0) {
8807
8899
  return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
@@ -8866,7 +8958,7 @@ function worktreeHoldsUnpushed(path, remote) {
8866
8958
  }
8867
8959
  async function finishRunWorktree(dir, cfg, branch) {
8868
8960
  const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
8869
- if (!wt || !existsSync14(wt.path)) return { kind: "none" };
8961
+ if (!wt || !existsSync15(wt.path)) return { kind: "none" };
8870
8962
  if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
8871
8963
  if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
8872
8964
  if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
@@ -8878,7 +8970,7 @@ async function finishRunWorktree(dir, cfg, branch) {
8878
8970
  if (pids.length) await killGently(pids);
8879
8971
  }
8880
8972
  gitOk(dir, ["worktree", "remove", wt.path]);
8881
- return existsSync14(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
8973
+ return existsSync15(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
8882
8974
  }
8883
8975
 
8884
8976
  // src/handoff-guard.ts
@@ -9114,12 +9206,12 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
9114
9206
 
9115
9207
  // src/connect-wizard.ts
9116
9208
  import { delimiter, dirname as dirname8, join as join17 } from "path";
9117
- import { accessSync as accessSync2, constants as constants2, existsSync as existsSync15, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
9209
+ import { accessSync as accessSync2, constants as constants2, existsSync as existsSync16, mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
9118
9210
  import { stringify } from "yaml";
9119
9211
  var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
9120
9212
  function shouldRunWizard(opts) {
9121
9213
  if (!opts.isTTY) return false;
9122
- const exists = opts.configExists ?? existsSync15;
9214
+ const exists = opts.configExists ?? existsSync16;
9123
9215
  return !configSearchPath(opts.crewHome).some((p) => exists(p));
9124
9216
  }
9125
9217
  function firstRunConfigPath() {
@@ -9231,7 +9323,7 @@ Wrote ${o.configPath}.
9231
9323
  }
9232
9324
 
9233
9325
  // src/release-run.ts
9234
- import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync16, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
9326
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync10, existsSync as existsSync17, copyFileSync, mkdirSync as mkdirSync8, mkdtempSync, rmSync as rmSync7 } from "fs";
9235
9327
  import { dirname as dirname9, join as join18 } from "path";
9236
9328
  import { tmpdir as tmpdir2 } from "os";
9237
9329
 
@@ -9922,7 +10014,7 @@ async function bump(o, size) {
9922
10014
  }
9923
10015
  const files = o.repo.release.versionFiles.map((f) => join18(o.cwd, f));
9924
10016
  const first = files[0];
9925
- if (!existsSync16(first)) {
10017
+ if (!existsSync17(first)) {
9926
10018
  o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
9927
10019
  return null;
9928
10020
  }
@@ -9958,7 +10050,7 @@ function writeChangelog(o, version, entries) {
9958
10050
  o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
9959
10051
  return;
9960
10052
  }
9961
- const existing = existsSync16(path) ? readFileSync13(path, "utf8") : "# Changelog\n";
10053
+ const existing = existsSync17(path) ? readFileSync13(path, "utf8") : "# Changelog\n";
9962
10054
  writeFileSync10(path, insertChangelogSection(existing, section));
9963
10055
  }
9964
10056
  async function confirm(o, expected) {
@@ -10064,7 +10156,7 @@ function openReleaseCheckout(o) {
10064
10156
  }
10065
10157
  for (const f of o.repo.worktrees.copy) {
10066
10158
  const src = join18(o.cwd, f);
10067
- if (!existsSync16(src)) continue;
10159
+ if (!existsSync17(src)) continue;
10068
10160
  mkdirSync8(dirname9(join18(dir, f)), { recursive: true });
10069
10161
  copyFileSync(src, join18(dir, f));
10070
10162
  }
@@ -10649,7 +10741,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
10649
10741
  }
10650
10742
 
10651
10743
  // src/watch.ts
10652
- import { watch as watchFile, existsSync as existsSync17, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
10744
+ import { watch as watchFile, existsSync as existsSync18, statSync as statSync6, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
10653
10745
  var E = "\x1B[";
10654
10746
  var dim = (s) => `${E}2m${s}${E}0m`;
10655
10747
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -10750,7 +10842,7 @@ function renderHeaders(states, now = Date.now()) {
10750
10842
  return states.map((s) => renderHeader(s, now)).join("");
10751
10843
  }
10752
10844
  function readFrom(file, from) {
10753
- if (!existsSync17(file)) return { events: [], offset: from };
10845
+ if (!existsSync18(file)) return { events: [], offset: from };
10754
10846
  const size = statSync6(file).size;
10755
10847
  if (size < from) return readFrom(file, 0);
10756
10848
  if (size === from) return { events: [], offset: from };
@@ -11769,7 +11861,7 @@ function schedulerInstalled(plan, exists, crontab) {
11769
11861
  }
11770
11862
 
11771
11863
  // src/worktree-sweep.ts
11772
- import { existsSync as existsSync18 } from "fs";
11864
+ import { existsSync as existsSync19 } from "fs";
11773
11865
  import { basename as basename8 } from "path";
11774
11866
  function planWorktreeSweep(target, tickets, contract) {
11775
11867
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
@@ -11805,7 +11897,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
11805
11897
  let removed = 0;
11806
11898
  let keptBranches = 0;
11807
11899
  for (const a of actions) {
11808
- if (!dryRun && existsSync18(a.path)) {
11900
+ if (!dryRun && existsSync19(a.path)) {
11809
11901
  let dirty = false;
11810
11902
  try {
11811
11903
  dirty = status(a.path).length > 0;
@@ -11819,7 +11911,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin")
11819
11911
  continue;
11820
11912
  }
11821
11913
  }
11822
- if (!dryRun && a.requirePushed && a.branch && existsSync18(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
11914
+ if (!dryRun && a.requirePushed && a.branch && existsSync19(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
11823
11915
  log.warn(
11824
11916
  `not removing worktree ${a.path} for ${a.ticket.issue_id} (${a.ticket.status}): ${a.branch} has commits that are not on ${remote}`,
11825
11917
  { ticket: a.ticket.issue_id, step: "worktree" }
@@ -11951,12 +12043,12 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
11951
12043
  }
11952
12044
 
11953
12045
  // src/stream-sweep.ts
11954
- import { existsSync as existsSync19, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
12046
+ import { existsSync as existsSync20, readdirSync as readdirSync5, rmSync as rmSync8, statSync as statSync7 } from "fs";
11955
12047
  import { join as join19 } from "path";
11956
12048
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
11957
12049
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
11958
12050
  const dir = join19(stateDir, "streams");
11959
- if (!existsSync19(dir)) return [];
12051
+ if (!existsSync20(dir)) return [];
11960
12052
  const cutoff = now - retentionDays * DAY_MS2;
11961
12053
  return readdirSync5(dir).map((name) => join19(dir, name)).filter((path) => {
11962
12054
  try {
@@ -11984,7 +12076,7 @@ function applyStreamSweep(paths, dryRun, log) {
11984
12076
  return removed;
11985
12077
  }
11986
12078
  function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
11987
- if (!existsSync19(stateDir)) return [];
12079
+ if (!existsSync20(stateDir)) return [];
11988
12080
  const cutoff = now - retentionDays * DAY_MS2;
11989
12081
  return readdirSync5(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join19(stateDir, name)).filter((path) => {
11990
12082
  try {
@@ -11996,13 +12088,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
11996
12088
  }
11997
12089
 
11998
12090
  // src/cli.ts
11999
- import { readFileSync as readFileSync18, existsSync as existsSync25, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
12091
+ import { readFileSync as readFileSync19, existsSync as existsSync26, mkdirSync as mkdirSync13, writeFileSync as writeFileSync14, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync9 } from "fs";
12000
12092
  import { createInterface } from "readline/promises";
12001
12093
  import { dirname as dirOf, resolve as resolvePath } from "path";
12002
12094
 
12003
12095
  // src/ssh-keys.ts
12004
12096
  import { execFileSync as execFileSync3 } from "child_process";
12005
- import { existsSync as existsSync20, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
12097
+ import { existsSync as existsSync21, mkdirSync as mkdirSync9, readFileSync as readFileSync14, chmodSync } from "fs";
12006
12098
  import { dirname as dirname11, join as join20 } from "path";
12007
12099
  var SshKeyError = class extends Error {
12008
12100
  };
@@ -12012,7 +12104,7 @@ function shipSshKeyPathFor(stateDir) {
12012
12104
  function ensureShipSshKeypair(stateDir) {
12013
12105
  const privateKeyPath = shipSshKeyPathFor(stateDir);
12014
12106
  const publicKeyPath = `${privateKeyPath}.pub`;
12015
- if (!existsSync20(privateKeyPath)) {
12107
+ if (!existsSync21(privateKeyPath)) {
12016
12108
  mkdirSync9(dirname11(privateKeyPath), { recursive: true });
12017
12109
  try {
12018
12110
  execFileSync3("ssh-keygen", [
@@ -12033,7 +12125,7 @@ function ensureShipSshKeypair(stateDir) {
12033
12125
  }
12034
12126
  chmodSync(privateKeyPath, 384);
12035
12127
  }
12036
- if (!existsSync20(publicKeyPath)) {
12128
+ if (!existsSync21(publicKeyPath)) {
12037
12129
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
12038
12130
  }
12039
12131
  const publicKey = readFileSync14(publicKeyPath, "utf8").trim();
@@ -12252,7 +12344,7 @@ function syncAllPassengerCheckouts(cfg) {
12252
12344
 
12253
12345
  // src/tunnel.ts
12254
12346
  import { spawn as spawn3 } from "child_process";
12255
- import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync6 } from "fs";
12347
+ import { existsSync as existsSync22, mkdirSync as mkdirSync10, readFileSync as readFileSync15, writeFileSync as writeFileSync11, unlinkSync as unlinkSync3, readdirSync as readdirSync6 } from "fs";
12256
12348
  import { randomInt } from "crypto";
12257
12349
  import { join as join21 } from "path";
12258
12350
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -12325,7 +12417,7 @@ function tunnelStatePath(stateDir, workspaceId) {
12325
12417
  }
12326
12418
  function readPersistedTunnel(stateDir, workspaceId) {
12327
12419
  const path = tunnelStatePath(stateDir, workspaceId);
12328
- if (!existsSync21(path)) return void 0;
12420
+ if (!existsSync22(path)) return void 0;
12329
12421
  try {
12330
12422
  return JSON.parse(readFileSync15(path, "utf8"));
12331
12423
  } catch {
@@ -12433,7 +12525,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
12433
12525
  }
12434
12526
  function readAllPersistedWorkspaceIds(stateDir) {
12435
12527
  const dir = join21(stateDir, "passengers");
12436
- if (!existsSync21(dir)) return [];
12528
+ if (!existsSync22(dir)) return [];
12437
12529
  return readdirSync6(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
12438
12530
  }
12439
12531
 
@@ -12554,7 +12646,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
12554
12646
  import { createServer as createHttpServer } from "http";
12555
12647
  import { spawn as spawn4 } from "child_process";
12556
12648
  import { randomBytes } from "crypto";
12557
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
12649
+ import { existsSync as existsSync23, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync12, unlinkSync as unlinkSync4, readdirSync as readdirSync7 } from "fs";
12558
12650
  import { join as join22 } from "path";
12559
12651
 
12560
12652
  // src/secret-compare.ts
@@ -12648,7 +12740,7 @@ function syncDaemonStatePath(stateDir, workspaceId) {
12648
12740
  }
12649
12741
  function readPersistedSyncDaemon(stateDir, workspaceId) {
12650
12742
  const path = syncDaemonStatePath(stateDir, workspaceId);
12651
- if (!existsSync22(path)) return void 0;
12743
+ if (!existsSync23(path)) return void 0;
12652
12744
  try {
12653
12745
  return JSON.parse(readFileSync16(path, "utf8"));
12654
12746
  } catch {
@@ -12679,7 +12771,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
12679
12771
  }
12680
12772
  function readAllPersistedSyncWorkspaceIds(stateDir) {
12681
12773
  const dir = join22(stateDir, "passengers");
12682
- if (!existsSync22(dir)) return [];
12774
+ if (!existsSync23(dir)) return [];
12683
12775
  const suffix = "-sync.json";
12684
12776
  return readdirSync7(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
12685
12777
  }
@@ -12736,11 +12828,11 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
12736
12828
  }
12737
12829
 
12738
12830
  // src/daemon.ts
12739
- import { existsSync as existsSync24, mkdirSync as mkdirSync12, readFileSync as readFileSync17, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12831
+ import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, readdirSync as readdirSync9, rmSync as rmSync9, statSync as statSync8, writeFileSync as writeFileSync13 } from "fs";
12740
12832
  import { join as join24 } from "path";
12741
12833
 
12742
12834
  // src/interrupted.ts
12743
- import { existsSync as existsSync23, readdirSync as readdirSync8 } from "fs";
12835
+ import { existsSync as existsSync24, readdirSync as readdirSync8, readFileSync as readFileSync17 } from "fs";
12744
12836
  import { join as join23 } from "path";
12745
12837
  var INTERRUPTED_MARKER = "<!-- crew:interrupted -->";
12746
12838
  function interruptedComment(b) {
@@ -12757,15 +12849,24 @@ var pidAlive = (pid) => {
12757
12849
  return e.code === "EPERM";
12758
12850
  }
12759
12851
  };
12852
+ function runFinished(streamPath) {
12853
+ const path = `${streamPath}.meta.json`;
12854
+ if (!existsSync24(path)) return false;
12855
+ try {
12856
+ return JSON.parse(readFileSync17(path, "utf8")).running !== true;
12857
+ } catch {
12858
+ return true;
12859
+ }
12860
+ }
12760
12861
  function orphanedStreams(o) {
12761
12862
  const dir = join23(o.stateDir, "streams");
12762
- if (!existsSync23(dir)) return [];
12863
+ if (!existsSync24(dir)) return [];
12763
12864
  const ownerAlive = o.isOwnerAlive ?? pidAlive;
12764
12865
  const out = [];
12765
12866
  for (const name of readdirSync8(dir)) {
12766
12867
  if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
12767
12868
  const streamPath = join23(dir, name);
12768
- if (existsSync23(`${streamPath}.meta.json`)) continue;
12869
+ if (runFinished(streamPath)) continue;
12769
12870
  const sidecar = readRunSidecar(streamPath);
12770
12871
  if (!sidecar || sidecar.route !== o.routeLabel) continue;
12771
12872
  if (ownerAlive(sidecar.ownerPid)) continue;
@@ -12873,7 +12974,7 @@ function computeTreeSignature(paths) {
12873
12974
  }
12874
12975
  function makeStaleChecker(paths, opts = {}) {
12875
12976
  const initial = computeTreeSignature(paths);
12876
- return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync24(opts.reloadSentinel);
12977
+ return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync25(opts.reloadSentinel);
12877
12978
  }
12878
12979
  function reloadSentinelFor(stateDir) {
12879
12980
  return join24(stateDir, "reload-requested");
@@ -12889,7 +12990,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
12889
12990
  }
12890
12991
  function readDaemonStarted(stateDir) {
12891
12992
  try {
12892
- const v = JSON.parse(readFileSync17(daemonStampFor(stateDir), "utf8")).startedAt;
12993
+ const v = JSON.parse(readFileSync18(daemonStampFor(stateDir), "utf8")).startedAt;
12893
12994
  return typeof v === "number" ? v : null;
12894
12995
  } catch {
12895
12996
  return null;
@@ -13759,7 +13860,7 @@ if (command === "passenger-sync-daemon") {
13759
13860
  if (host === "linux" && hasSystemd) {
13760
13861
  const label = labelFor(CREW_HOME, "run");
13761
13862
  const staleTimer = join25(homedir8(), ".config", "systemd", "user", `${label}.timer`);
13762
- if (existsSync25(staleTimer)) {
13863
+ if (existsSync26(staleTimer)) {
13763
13864
  process.stdout.write(
13764
13865
  `removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
13765
13866
  `
@@ -13785,7 +13886,7 @@ if (command === "passenger-sync-daemon") {
13785
13886
  const tracker = new Tracker(c, cfg.ship);
13786
13887
  problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
13787
13888
  for (const r of await resolvedRepos(c)) {
13788
- if (!existsSync25(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
13889
+ if (!existsSync26(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
13789
13890
  if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
13790
13891
  problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
13791
13892
  }
@@ -13803,7 +13904,7 @@ if (command === "passenger-sync-daemon") {
13803
13904
  }
13804
13905
  async function doctorFix(o) {
13805
13906
  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))));
13806
- const unitExists = (p) => existsSync25(p);
13907
+ const unitExists = (p) => existsSync26(p);
13807
13908
  const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
13808
13909
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
13809
13910
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
@@ -13820,7 +13921,7 @@ if (command === "passenger-sync-daemon") {
13820
13921
  for (const a of plan.actions) {
13821
13922
  if (a.kind === "enable") {
13822
13923
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
13823
- writeFileSync14(cfg.configFile, planEnable(readFileSync18(cfg.configFile, "utf8"), a.route));
13924
+ writeFileSync14(cfg.configFile, planEnable(readFileSync19(cfg.configFile, "utf8"), a.route));
13824
13925
  enabledSound.add(a.route);
13825
13926
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
13826
13927
  } else {
@@ -14958,7 +15059,7 @@ ${changed2.map((c) => ` - ${c}
14958
15059
  `);
14959
15060
  if (!dryRun) {
14960
15061
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
14961
- const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
15062
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
14962
15063
  raw.agentPersonas = personaResult.agentPersonas;
14963
15064
  raw.agentSkills = skillResult.agentSkills;
14964
15065
  if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
@@ -15018,7 +15119,7 @@ ${changed2.map((c) => ` - ${c}
15018
15119
  `);
15019
15120
  if (!dryRun) {
15020
15121
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
15021
- const raw = existsSync25(resolvedPath) ? JSON.parse(readFileSync18(resolvedPath, "utf8")) : {};
15122
+ const raw = existsSync26(resolvedPath) ? JSON.parse(readFileSync19(resolvedPath, "utf8")) : {};
15022
15123
  raw.agentSkills = result.agentSkills;
15023
15124
  mkdirSync13(dirOf(resolvedPath), { recursive: true });
15024
15125
  writeFileSync14(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -15034,14 +15135,14 @@ ${changed2.map((c) => ` - ${c}
15034
15135
  process.exit(2);
15035
15136
  };
15036
15137
  if (sub !== "add" && sub !== "list") usage2();
15037
- const configText = readFileSync18(cfg.configFile, "utf8");
15138
+ const configText = readFileSync19(cfg.configFile, "utf8");
15038
15139
  let routeName = positional[2];
15039
15140
  let addPath = ".";
15040
15141
  let addName;
15041
15142
  if (sub === "list" && !routeName) usage2();
15042
15143
  if (sub === "add") {
15043
15144
  try {
15044
- const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync25(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
15145
+ const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync26(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
15045
15146
  routeName = a.route;
15046
15147
  addPath = a.path;
15047
15148
  addName = a.name;
@@ -15056,9 +15157,9 @@ ${changed2.map((c) => ` - ${c}
15056
15157
  }
15057
15158
  const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
15058
15159
  let resolvedRemotes;
15059
- if (existsSync25(resolvedFile)) {
15160
+ if (existsSync26(resolvedFile)) {
15060
15161
  try {
15061
- resolvedRemotes = JSON.parse(readFileSync18(resolvedFile, "utf8"));
15162
+ resolvedRemotes = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15062
15163
  } catch {
15063
15164
  }
15064
15165
  }
@@ -15073,14 +15174,14 @@ ${changed2.map((c) => ` - ${c}
15073
15174
  for (const r of repos) {
15074
15175
  const origin2 = originOf(r.dir);
15075
15176
  const row = matchRepoName(origin2, resolvedRemotes);
15076
- const note = !existsSync25(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
15177
+ const note = !existsSync26(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
15077
15178
  process.stdout.write(`${r.name} ${r.dir} ${note}
15078
15179
  `);
15079
15180
  }
15080
15181
  break;
15081
15182
  }
15082
15183
  const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
15083
- const top = existsSync25(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
15184
+ const top = existsSync26(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
15084
15185
  if (!top) throw new ReposError(`${target} is not a git checkout`);
15085
15186
  const origin = originOf(top);
15086
15187
  const plan = planRepoAdd({
@@ -15129,8 +15230,8 @@ ${changed2.map((c) => ` - ${c}
15129
15230
  const created = await rowTracker.createRepoRow(rowPayload);
15130
15231
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
15131
15232
  `);
15132
- if (existsSync25(resolvedFile)) {
15133
- const raw = JSON.parse(readFileSync18(resolvedFile, "utf8"));
15233
+ if (existsSync26(resolvedFile)) {
15234
+ const raw = JSON.parse(readFileSync19(resolvedFile, "utf8"));
15134
15235
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
15135
15236
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
15136
15237
  writeFileSync14(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -15376,10 +15477,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
15376
15477
  operator = await pickOperator(found.holds);
15377
15478
  }
15378
15479
  let previous;
15379
- const hadPreviousFile = existsSync25(resolvedPath);
15480
+ const hadPreviousFile = existsSync26(resolvedPath);
15380
15481
  if (hadPreviousFile) {
15381
15482
  try {
15382
- previous = JSON.parse(readFileSync18(resolvedPath, "utf8"));
15483
+ previous = JSON.parse(readFileSync19(resolvedPath, "utf8"));
15383
15484
  } catch {
15384
15485
  }
15385
15486
  }
@@ -16071,7 +16172,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
16071
16172
  }
16072
16173
  case "log": {
16073
16174
  try {
16074
- const text = readFileSync18(cfg.ship.logFile, "utf8").split("\n");
16175
+ const text = readFileSync19(cfg.ship.logFile, "utf8").split("\n");
16075
16176
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
16076
16177
  `);
16077
16178
  } catch {
@@ -16281,7 +16382,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16281
16382
  );
16282
16383
  try {
16283
16384
  const evFile = eventFileFor(cfg.ship.stateDir);
16284
- const evSize = existsSync25(evFile) ? statSync9(evFile).size : 0;
16385
+ const evSize = existsSync26(evFile) ? statSync9(evFile).size : 0;
16285
16386
  process.stdout.write(
16286
16387
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
16287
16388
  ` : "events log: ok\n"
@@ -16292,7 +16393,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16292
16393
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
16293
16394
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
16294
16395
  try {
16295
- return readFileSync18(p, "utf8");
16396
+ return readFileSync19(p, "utf8");
16296
16397
  } catch {
16297
16398
  return null;
16298
16399
  }
@@ -16339,12 +16440,10 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
16339
16440
  process.stdout.write(` - ${p2}
16340
16441
  `);
16341
16442
  }
16342
- const bh = baseHealth(r);
16343
- if (bh.action !== "not-applicable") {
16344
- process.stdout.write(` base ${config.branch.base}: ${bh.unsafe ? "ERROR \u2014 " : ""}${bh.detail}
16345
- `);
16346
- if (bh.unsafe) process.exitCode = 1;
16347
- }
16443
+ const dh = doctorBaseReport(r, config.branch.base, baseHealth);
16444
+ process.stdout.write(dh.text);
16445
+ if (dh.failed) process.exitCode = 1;
16446
+ if (dh.skipRest) continue;
16348
16447
  if (config.release.ship) {
16349
16448
  process.stdout.write(` release.ship ${config.release.ship}${config.release.ship === cfg.ship.name ? " (this ship)" : ""}
16350
16449
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.50.0",
3
+ "version": "0.50.2",
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",