@tablation/crew 0.77.0 → 0.78.1

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 +1 -0
  2. package/dist/cli.js +268 -68
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -263,6 +263,7 @@ crew inbox [--member NAME] your tickets across every workspace
263
263
  crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
264
264
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
265
265
  crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
266
+ crew repos check [PATH] validate a checkout's .crew.yaml (parse, release readiness, hook syntax) without attaching it (--route ROUTE, --strict, --json)
266
267
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo [--replace] writes them, --clear route removes them, --recipes, --hook KEY=CMD, --agent, --json)
267
268
  crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
268
269
  crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
package/dist/cli.js CHANGED
@@ -230,6 +230,13 @@ function extractTurnTokens(line) {
230
230
  if (!usage2) return void 0;
231
231
  return (usage2.input_tokens ?? 0) + (usage2.output_tokens ?? 0) + (usage2.cache_read_input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
232
232
  }
233
+ function extractPrefixTokens(line) {
234
+ const obj = line;
235
+ if (!obj || obj.type !== "assistant") return void 0;
236
+ const usage2 = obj.message?.usage;
237
+ if (!usage2) return void 0;
238
+ return (usage2.input_tokens ?? 0) + (usage2.cache_creation_input_tokens ?? 0);
239
+ }
233
240
  function extractResult(line) {
234
241
  const obj = line;
235
242
  if (!obj || obj.type !== "result") return void 0;
@@ -580,6 +587,7 @@ var claudeAdapter = {
580
587
  mapStreamLine,
581
588
  extractResult,
582
589
  extractTurnTokens,
590
+ extractPrefixTokens,
583
591
  inspectMcp(o) {
584
592
  const s = inspectClaudeMcp(o);
585
593
  return s.state === "no-claude" ? { state: "no-binary" } : s;
@@ -5114,6 +5122,8 @@ async function reportAgentRun(target, run2) {
5114
5122
  cacheReadTokens: run2.cacheReadTokens,
5115
5123
  cacheWriteTokens: run2.cacheWriteTokens,
5116
5124
  costUsd: run2.costUsd,
5125
+ numTurns: run2.numTurns,
5126
+ prefixTokens: run2.prefixTokens,
5117
5127
  promptVersion: run2.promptVersion,
5118
5128
  promptSha: run2.promptSha
5119
5129
  };
@@ -5271,24 +5281,31 @@ function ticketMentions(text) {
5271
5281
  }
5272
5282
  var TicketAttributionTracker = class {
5273
5283
  #tokensByTicket = /* @__PURE__ */ new Map();
5284
+ #turns = 0;
5274
5285
  #current;
5275
5286
  /** Tracker record uuid → ticket key, learned when a write names both (a claim resolves the key from the last mention). */
5276
5287
  #keyByRecord = /* @__PURE__ */ new Map();
5277
5288
  #lastBy = {};
5278
- /** The ticket key most recently named in the transcript, or undefined before any. */
5289
+ /**
5290
+ * The ticket key most recently named in the transcript, or undefined before any.
5291
+ * Only the FIRST key in a turn counts: a closing summary names its own
5292
+ * ticket first and then lists others ("Other tickets", "Blocked by", "See
5293
+ * also"), and those trailing keys are not what the run worked (CREW-2598).
5294
+ */
5279
5295
  get current() {
5280
5296
  return this.#current;
5281
5297
  }
5282
5298
  /** Record the mentions in `text` without crediting tokens (a claim needs "which ticket" before the turn's usage line arrives). */
5283
5299
  note(text) {
5284
5300
  const mentions = ticketMentions(text);
5285
- if (mentions.length > 0) this.#current = mentions[mentions.length - 1];
5301
+ if (mentions.length > 0) this.#current = mentions[0];
5286
5302
  }
5287
5303
  /** One assistant turn: its combined visible text (thinking + text blocks) and its own token cost. */
5288
5304
  add(text, tokens) {
5289
5305
  const mentions = ticketMentions(text);
5290
- if (mentions.length > 0) this.#current = mentions[mentions.length - 1];
5306
+ if (mentions.length > 0) this.#current = mentions[0];
5291
5307
  if (this.#current === void 0 || tokens <= 0) return;
5308
+ this.#turns++;
5292
5309
  this.#tokensByTicket.set(this.#current, (this.#tokensByTicket.get(this.#current) ?? 0) + tokens);
5293
5310
  }
5294
5311
  /**
@@ -5310,16 +5327,31 @@ var TicketAttributionTracker = class {
5310
5327
  /**
5311
5328
  * How the attribution is decided: a hand-off write beats a claim, a claim
5312
5329
  * beats a comment (so a follow-up ticket filed or commented on while
5313
- * working never displaces the claimed one), and any write beats the
5314
- * mention-token heuristic, which is only the fallback for runs that made
5315
- * no ticket-targeted write. The pre-run hint is last.
5330
+ * working never displaces the claimed one), and any write beats everything
5331
+ * below. Next, `hintMoved` — the post-run read-back showed the hinted
5332
+ * ticket changed status or assignee during the run — is a hand-off signal
5333
+ * for a run that wrote through MCP tools crew cannot see. Then the
5334
+ * mention-token heuristic, which with a hint may override it only when a
5335
+ * different ticket holds a strict majority of the credited tokens (a single
5336
+ * credited turn, like a lone end-of-run summary naming another ticket,
5337
+ * must not). A run with no hint
5338
+ * keeps the plain winner. The pre-run hint is last.
5316
5339
  */
5317
- decide(hint) {
5340
+ decide(hint, opts = {}) {
5318
5341
  for (const by of ["handoff", "claim", "comment"]) {
5319
5342
  const ticket = this.#lastBy[by];
5320
5343
  if (ticket) return { ticket, by };
5321
5344
  }
5322
- if (this.#tokensByTicket.size > 0) return { ticket: this.winner(hint), by: "mentions" };
5345
+ if (hint && opts.hintMoved) return { ticket: hint, by: "readback" };
5346
+ if (this.#tokensByTicket.size > 0) {
5347
+ if (!hint) return { ticket: this.winner(), by: "mentions" };
5348
+ const winner = this.winner(hint);
5349
+ if (winner === hint) return { ticket: hint, by: "mentions" };
5350
+ let total = 0;
5351
+ for (const n of this.#tokensByTicket.values()) total += n;
5352
+ const share = (this.#tokensByTicket.get(winner) ?? 0) / total;
5353
+ return share > 0.5 && this.#turns > 1 ? { ticket: winner, by: "mentions" } : { ticket: hint, by: "hint" };
5354
+ }
5323
5355
  return { ticket: hint, by: "hint" };
5324
5356
  }
5325
5357
  /**
@@ -8467,6 +8499,8 @@ var RunObserver = class {
8467
8499
  /** Hand-off writes and their outcomes, to tell a run whose hand-off never landed (CREW-2363). */
8468
8500
  handoff = new HandoffTranscriptTracker();
8469
8501
  result;
8502
+ /** Prefix tokens of the first assistant call that carried usage; kept once. */
8503
+ prefixTokens;
8470
8504
  /** Set once the stream carries the harness's final result (turn count or cost). */
8471
8505
  sawResult = false;
8472
8506
  muted = false;
@@ -8593,6 +8627,7 @@ var RunObserver = class {
8593
8627
  }
8594
8628
  }
8595
8629
  }
8630
+ if (this.prefixTokens === void 0 && parsed !== void 0) this.prefixTokens = this.adapter.extractPrefixTokens?.(parsed);
8596
8631
  const turnTokens = parsed === void 0 ? void 0 : this.adapter.extractTurnTokens(parsed);
8597
8632
  if (turnTokens !== void 0) this.attribution.add(turnText, turnTokens);
8598
8633
  }
@@ -8616,8 +8651,15 @@ function writeRunMeta(id, startedAt, liveTicket, extra) {
8616
8651
  } catch {
8617
8652
  }
8618
8653
  }
8619
- async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8620
- const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket);
8654
+ async function finishRun(id, obs, emit, startedAt, end, agentLogKey, hintMoved) {
8655
+ let moved = false;
8656
+ if (hintMoved) {
8657
+ try {
8658
+ moved = await hintMoved();
8659
+ } catch {
8660
+ }
8661
+ }
8662
+ const { ticket: attributedTicket, by: attributedBy } = obs.attribution.decide(obs.liveTicket, { hintMoved: moved });
8621
8663
  const identity = { toolchain: id.toolchain, harness: id.harness, provider: id.provider };
8622
8664
  const { result } = obs;
8623
8665
  const pending = end.interrupted ? null : obs.handoff.pending();
@@ -8649,6 +8691,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8649
8691
  ...pending ? { handoff: "pending" } : {},
8650
8692
  sessionId: result?.sessionId ?? null,
8651
8693
  numTurns: result?.numTurns ?? null,
8694
+ prefixTokens: obs.prefixTokens ?? null,
8652
8695
  totalCostUsd: result?.costUsd ?? null
8653
8696
  });
8654
8697
  } catch {
@@ -8661,6 +8704,8 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8661
8704
  ...pending ? { handoff: "pending" } : {},
8662
8705
  ...end.interrupted ? { interrupted: end.interrupted } : {},
8663
8706
  ...identity,
8707
+ attributedTicket: attributedTicket ?? null,
8708
+ attributedBy,
8664
8709
  ...result?.sessionId ? { sessionId: result.sessionId } : {},
8665
8710
  ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
8666
8711
  ...result?.costUsd !== void 0 ? { totalCostUsd: result.costUsd } : {}
@@ -8679,6 +8724,9 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8679
8724
  cacheReadTokens: result?.cacheReadTokens,
8680
8725
  cacheWriteTokens: result?.cacheWriteTokens,
8681
8726
  costUsd: result?.costUsd,
8727
+ // Both come from a finished run: without the result line (interrupted) neither is trustworthy.
8728
+ ...result?.numTurns !== void 0 ? { numTurns: result.numTurns } : {},
8729
+ ...result?.numTurns !== void 0 && obs.prefixTokens !== void 0 ? { prefixTokens: obs.prefixTokens } : {},
8682
8730
  harness: identity.harness,
8683
8731
  provider: identity.provider,
8684
8732
  promptVersion: id.promptVersion,
@@ -8723,7 +8771,7 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8723
8771
  }
8724
8772
  return { ms: ms2, ticket: attributedTicket, handoffPending: !!pending };
8725
8773
  }
8726
- function spawnAgent(plan, emit) {
8774
+ function spawnAgent(plan, emit, hintMoved) {
8727
8775
  const adapter = adapterFor(plan.harness ?? LEGACY_TOOLCHAIN_HARNESS);
8728
8776
  const started = Date.now();
8729
8777
  const startedAt = new Date(started).toISOString();
@@ -8788,7 +8836,7 @@ function spawnAgent(plan, emit) {
8788
8836
  obs.end();
8789
8837
  await endSink(eventsSink);
8790
8838
  if (spawnFailed) return;
8791
- const { ms: ms2, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey);
8839
+ const { ms: ms2, handoffPending } = await finishRun(id, obs, emit, startedAt, { code: code ?? 1 }, plan.agentLog?.apiKey, hintMoved);
8792
8840
  resolve6({ code: code ?? 1, ms: ms2, ...handoffPending ? { handoffPending } : {} });
8793
8841
  });
8794
8842
  child.stdin?.on("error", (err) => {
@@ -9041,10 +9089,10 @@ function doctorSelftestLines(stored, toolchains) {
9041
9089
  }
9042
9090
 
9043
9091
  // src/cli.ts
9044
- import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename11 } from "path";
9092
+ import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename12 } from "path";
9045
9093
  import { fileURLToPath } from "url";
9046
9094
  import { hostname as hostname2, homedir as homedir10 } from "os";
9047
- import { spawnSync as spawnSync6 } from "child_process";
9095
+ import { spawnSync as spawnSync7 } from "child_process";
9048
9096
  import { parse as parseYaml2 } from "yaml";
9049
9097
 
9050
9098
  // src/argv.ts
@@ -14393,12 +14441,107 @@ async function showLogEntry(route, entryId, opts = {}) {
14393
14441
  };
14394
14442
  }
14395
14443
 
14444
+ // src/repos-check.ts
14445
+ import { spawnSync as spawnSync6 } from "child_process";
14446
+ import { readFileSync as readFileSync24 } from "fs";
14447
+ import { basename as basename8 } from "path";
14448
+ var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
14449
+ function checkHookSyntax(script, shell) {
14450
+ const spec = resolveShell(shell);
14451
+ const name = basename8(spec.bin).replace(/\.exe$/i, "").toLowerCase();
14452
+ let run2;
14453
+ if (POSIX_SHELL.test(name)) {
14454
+ run2 = spawnSync6(spec.bin, ["-n"], { input: script, encoding: "utf8" });
14455
+ } else if (/^(pwsh|powershell)$/.test(name)) {
14456
+ const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
14457
+ run2 = spawnSync6(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
14458
+ encoding: "utf8",
14459
+ env: { ...process.env, CREW_CHECK_SCRIPT: script }
14460
+ });
14461
+ } else {
14462
+ return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
14463
+ }
14464
+ if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
14465
+ if (run2.status === 0) return { status: "ok" };
14466
+ const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
14467
+ return { status: "error", message };
14468
+ }
14469
+ function renderSamples(cfg, dir) {
14470
+ const checkout = basename8(dir.replace(/[/\\]+$/, ""));
14471
+ const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
14472
+ const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
14473
+ return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
14474
+ const branch = renderBranchName(effectiveBranchTemplate(cfg, ctx.prefix), ctx);
14475
+ const worktree = effectiveWorktreeDirName(cfg, dir, branch, "123", (t) => renderWorktreeName(t, { ...ctx, dir: checkout }));
14476
+ return { case: label, branch, worktree };
14477
+ });
14478
+ }
14479
+ function checkRepoSpec(dir, opts = {}) {
14480
+ const empty = { parsed: null, effective: null, readiness: [], hooks: [], shadowed: [], samples: [] };
14481
+ const file = findRepoConfig(dir);
14482
+ if (!file) {
14483
+ return { ...empty, file: null, error: `no .crew.yaml, .crew.yml or .crew.json in ${dir}`, exitCode: 1 };
14484
+ }
14485
+ let parsed;
14486
+ try {
14487
+ parsed = parseRepoConfig(readFileSync24(file, "utf8"), file);
14488
+ } catch (e) {
14489
+ if (!(e instanceof RepoConfigError)) throw e;
14490
+ return { ...empty, file, error: e.message, exitCode: 1 };
14491
+ }
14492
+ const effective = resolveRepoConfig(parsed, opts.ship, dir);
14493
+ const readiness = validateEffective(effective);
14494
+ const hooks = Object.keys(effective.hooks).map((name) => ({
14495
+ name,
14496
+ label: hookLabel(effective, name),
14497
+ syntax: checkHookSyntax(effective.hooks[name], effective.shell)
14498
+ }));
14499
+ const failed = hooks.some((h) => h.syntax.status === "error");
14500
+ return {
14501
+ file,
14502
+ parsed,
14503
+ effective,
14504
+ readiness,
14505
+ hooks,
14506
+ shadowed: effective.shadowed,
14507
+ samples: renderSamples(effective, dir),
14508
+ error: null,
14509
+ exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
14510
+ };
14511
+ }
14512
+ function renderRepoCheck(r) {
14513
+ if (r.error) return `${r.file ?? "crew repos check"}: ${r.error}
14514
+ `;
14515
+ const e = r.effective;
14516
+ const lines = [
14517
+ `${r.file}`,
14518
+ ` platform ${e.platform}`,
14519
+ ` release.mode ${e.release.mode}`,
14520
+ ` release.versioning ${e.release.versioning}`,
14521
+ ` branch.base ${e.branch.base}`,
14522
+ ` branch.name ${e.branch.name}`,
14523
+ ` branch.push ${e.branch.push}`
14524
+ ];
14525
+ for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
14526
+ lines.push(r.hooks.length ? " hooks" : " hooks none");
14527
+ for (const h of r.hooks) {
14528
+ const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
14529
+ const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
14530
+ lines.push(` ${h.name}${named}: ${syntax}`);
14531
+ if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
14532
+ }
14533
+ if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
14534
+ for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
14535
+ return `${lines.join("\n")}
14536
+ `;
14537
+ }
14538
+
14396
14539
  // src/repos-hooks.ts
14397
14540
  import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
14398
14541
 
14399
14542
  // src/repos-cmd.ts
14400
14543
  import { homedir as homedir9 } from "os";
14401
- import { basename as basename8, join as join28, resolve as resolve3 } from "path";
14544
+ import { basename as basename9, join as join28, resolve as resolve3 } from "path";
14402
14545
  import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
14403
14546
  var ReposError = class extends Error {
14404
14547
  };
@@ -14470,7 +14613,7 @@ function configuredRepos(text, route, base) {
14470
14613
  });
14471
14614
  }
14472
14615
  const dir = m.get("dir");
14473
- return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
14616
+ return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
14474
14617
  }
14475
14618
  function cloneBaseOf(text, route, base, isCheckout) {
14476
14619
  const m = routeMapOf(parseDocument2(text), route);
@@ -14559,7 +14702,7 @@ function planRepoAdd(i) {
14559
14702
  throw new ReposError(`${target} is already configured on route ${i.route}`);
14560
14703
  }
14561
14704
  const matched = matchRepoName(i.origin, i.resolved);
14562
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
14705
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
14563
14706
  let warning;
14564
14707
  if (!matched) {
14565
14708
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -15075,7 +15218,7 @@ ${run2.err}` });
15075
15218
  }
15076
15219
 
15077
15220
  // src/sample-ticket.ts
15078
- import { basename as basename9, dirname as dirname17, join as join29 } from "path";
15221
+ import { basename as basename10, dirname as dirname17, join as join29 } from "path";
15079
15222
  var SAMPLE_REPO = "crew-sample";
15080
15223
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
15081
15224
  var SAMPLE_MARKER = "sample: true";
@@ -15191,8 +15334,8 @@ function schedulerInstalled(plan, exists, crontab) {
15191
15334
  }
15192
15335
 
15193
15336
  // src/worktree-sweep.ts
15194
- import { existsSync as existsSync27, readFileSync as readFileSync24, readdirSync as readdirSync8 } from "fs";
15195
- import { basename as basename10, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
15337
+ import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync8 } from "fs";
15338
+ import { basename as basename11, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
15196
15339
  function planWorktreeSweep(target, tickets, contract) {
15197
15340
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
15198
15341
  const verified = contract.statuses.verified;
@@ -15223,7 +15366,7 @@ function ticketForWorktree(w, byNumber, contract) {
15223
15366
  const t = num ? byNumber.get(num) : void 0;
15224
15367
  if (t && ticketStems(t).has(stem)) return t;
15225
15368
  }
15226
- const dir = basename10(w.path);
15369
+ const dir = basename11(w.path);
15227
15370
  const m = DIR_NUMBER.exec(dir);
15228
15371
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
15229
15372
  if (!w.branch) {
@@ -15239,7 +15382,7 @@ function worktreeTicketNumbers(target) {
15239
15382
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
15240
15383
  if (n) nums.add(n);
15241
15384
  }
15242
- const m = DIR_NUMBER_SUFFIXED.exec(basename10(w.path));
15385
+ const m = DIR_NUMBER_SUFFIXED.exec(basename11(w.path));
15243
15386
  if (m) nums.add(m[1]);
15244
15387
  }
15245
15388
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
@@ -15247,7 +15390,7 @@ function worktreeTicketNumbers(target) {
15247
15390
  }
15248
15391
  function siblingWorktreeDirs(target) {
15249
15392
  const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
15250
- const stem = `${basename10(target.dir.replace(/[/\\]+$/, ""))}-`;
15393
+ const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
15251
15394
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
15252
15395
  let names;
15253
15396
  try {
@@ -15272,7 +15415,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
15272
15415
  }
15273
15416
  function warnOrphanedWorktreeDirs(paths, log, held) {
15274
15417
  for (const p of paths) {
15275
- const number = DIR_NUMBER_SUFFIXED.exec(basename10(p))?.[1];
15418
+ const number = DIR_NUMBER_SUFFIXED.exec(basename11(p))?.[1];
15276
15419
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
15277
15420
  if (when) {
15278
15421
  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" });
@@ -15362,7 +15505,7 @@ function isHandBackMerge(path, base) {
15362
15505
  if (!msgPath) return false;
15363
15506
  let first;
15364
15507
  try {
15365
- first = readFileSync24(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
15508
+ first = readFileSync25(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
15366
15509
  } catch {
15367
15510
  return false;
15368
15511
  }
@@ -15416,7 +15559,7 @@ function isTicketBranch(name, base, stems) {
15416
15559
  }
15417
15560
  function sweepCandidatesExist(cwd, base) {
15418
15561
  const linked = worktrees(cwd).slice(1);
15419
- if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename10(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
15562
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename11(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
15420
15563
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
15421
15564
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
15422
15565
  }
@@ -15512,13 +15655,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
15512
15655
  }
15513
15656
 
15514
15657
  // src/cli.ts
15515
- import { realpathSync as realpathSync3, readFileSync as readFileSync30, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
15658
+ import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
15516
15659
  import { createInterface } from "readline/promises";
15517
15660
  import { dirname as dirOf, resolve as resolvePath } from "path";
15518
15661
 
15519
15662
  // src/ssh-keys.ts
15520
15663
  import { execFileSync as execFileSync6 } from "child_process";
15521
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, chmodSync } from "fs";
15664
+ import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync26, chmodSync } from "fs";
15522
15665
  import { dirname as dirname19, join as join32 } from "path";
15523
15666
  var SshKeyError = class extends Error {
15524
15667
  };
@@ -15552,7 +15695,7 @@ function ensureShipSshKeypair(stateDir) {
15552
15695
  if (!existsSync29(publicKeyPath)) {
15553
15696
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
15554
15697
  }
15555
- const publicKey = readFileSync25(publicKeyPath, "utf8").trim();
15698
+ const publicKey = readFileSync26(publicKeyPath, "utf8").trim();
15556
15699
  return { privateKeyPath, publicKeyPath, publicKey };
15557
15700
  }
15558
15701
  function sshKeygenAvailable() {
@@ -15769,7 +15912,7 @@ function syncAllPassengerCheckouts(cfg) {
15769
15912
 
15770
15913
  // src/tunnel.ts
15771
15914
  import { spawn as spawn5 } from "child_process";
15772
- import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15915
+ import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync27, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15773
15916
  import { randomInt } from "crypto";
15774
15917
  import { join as join33 } from "path";
15775
15918
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -15844,7 +15987,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
15844
15987
  const path = tunnelStatePath(stateDir, workspaceId);
15845
15988
  if (!existsSync30(path)) return void 0;
15846
15989
  try {
15847
- return JSON.parse(readFileSync26(path, "utf8"));
15990
+ return JSON.parse(readFileSync27(path, "utf8"));
15848
15991
  } catch {
15849
15992
  return void 0;
15850
15993
  }
@@ -16071,7 +16214,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
16071
16214
  import { createServer as createHttpServer } from "http";
16072
16215
  import { spawn as spawn6 } from "child_process";
16073
16216
  import { randomBytes } from "crypto";
16074
- import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync27, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
16217
+ import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
16075
16218
  import { join as join34 } from "path";
16076
16219
 
16077
16220
  // src/secret-compare.ts
@@ -16167,7 +16310,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
16167
16310
  const path = syncDaemonStatePath(stateDir, workspaceId);
16168
16311
  if (!existsSync31(path)) return void 0;
16169
16312
  try {
16170
- return JSON.parse(readFileSync27(path, "utf8"));
16313
+ return JSON.parse(readFileSync28(path, "utf8"));
16171
16314
  } catch {
16172
16315
  return void 0;
16173
16316
  }
@@ -16284,11 +16427,11 @@ function describeCause(err) {
16284
16427
  }
16285
16428
 
16286
16429
  // src/daemon.ts
16287
- import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync29, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
16430
+ import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
16288
16431
  import { join as join36 } from "path";
16289
16432
 
16290
16433
  // src/interrupted.ts
16291
- import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync28 } from "fs";
16434
+ import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync29 } from "fs";
16292
16435
  import { join as join35 } from "path";
16293
16436
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
16294
16437
  function interruptedComment(b) {
@@ -16310,7 +16453,7 @@ function runFinished(streamPath) {
16310
16453
  const path = `${streamPath}.meta.json`;
16311
16454
  if (!existsSync32(path)) return false;
16312
16455
  try {
16313
- return JSON.parse(readFileSync28(path, "utf8")).running !== true;
16456
+ return JSON.parse(readFileSync29(path, "utf8")).running !== true;
16314
16457
  } catch {
16315
16458
  return true;
16316
16459
  }
@@ -16467,7 +16610,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
16467
16610
  }
16468
16611
  function readDaemonStarted(stateDir) {
16469
16612
  try {
16470
- const v = JSON.parse(readFileSync29(daemonStampFor(stateDir), "utf8")).startedAt;
16613
+ const v = JSON.parse(readFileSync30(daemonStampFor(stateDir), "utf8")).startedAt;
16471
16614
  return typeof v === "number" ? v : null;
16472
16615
  } catch {
16473
16616
  return null;
@@ -16819,7 +16962,19 @@ async function runRoleAgent(role, decision, ctx) {
16819
16962
  }
16820
16963
  try {
16821
16964
  const runStart = (/* @__PURE__ */ new Date()).toISOString();
16822
- await spawnAgent(plan, emit);
16965
+ let hintMoved;
16966
+ if (workingId) {
16967
+ try {
16968
+ const before = await tracker.ticket(workingId);
16969
+ const col = tracker.contract.columns;
16970
+ hintMoved = async () => {
16971
+ const after = await tracker.ticket(workingId);
16972
+ return after[col.status] !== before[col.status] || after[col.assignee] !== before[col.assignee];
16973
+ };
16974
+ } catch {
16975
+ }
16976
+ }
16977
+ await spawnAgent(plan, emit, hintMoved);
16823
16978
  await guardRunHandoff(route, ship, tracker, role, workingId, emit, runStart);
16824
16979
  } finally {
16825
16980
  if (memberId) {
@@ -17018,6 +17173,7 @@ function usage(problem, requested = false) {
17018
17173
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
17019
17174
  crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
17020
17175
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
17176
+ crew repos check [PATH] validate a checkout's .crew.yaml without attaching it (--route ROUTE, --strict, --json)
17021
17177
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
17022
17178
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
17023
17179
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches (--json: every Repos row too)
@@ -17116,6 +17272,22 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
17116
17272
  "worktree"
17117
17273
  ]);
17118
17274
  if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
17275
+ function runReposCheck(dir, ship) {
17276
+ const report = checkRepoSpec(dir, { ship, strict: flag("strict") });
17277
+ if (flag("json")) {
17278
+ const { exitCode: _exit, ...rest } = report;
17279
+ process.stdout.write(`${JSON.stringify(rest)}
17280
+ `);
17281
+ } else {
17282
+ process.stdout.write(renderRepoCheck(report));
17283
+ }
17284
+ process.exitCode = report.exitCode;
17285
+ }
17286
+ var reposCheckBare = command === "repos" && positional[1] === "check" && !value("route") && positional.length < 4;
17287
+ if (reposCheckBare) {
17288
+ runReposCheck(resolvePath(positional[2] ?? "."));
17289
+ process.exit(process.exitCode ?? 0);
17290
+ }
17119
17291
  function findSyncRoute(c, name) {
17120
17292
  if (!name && c.routes.length > 1) {
17121
17293
  throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
@@ -17502,7 +17674,7 @@ if (command === "passenger-sync-daemon") {
17502
17674
  `
17503
17675
  );
17504
17676
  if (!dryRun) {
17505
- spawnSync6("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
17677
+ spawnSync7("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
17506
17678
  try {
17507
17679
  unlinkSync5(staleTimer);
17508
17680
  } catch {
@@ -17542,7 +17714,7 @@ if (command === "passenger-sync-daemon") {
17542
17714
  async function doctorFix(o) {
17543
17715
  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))));
17544
17716
  const unitExists = (p) => existsSync34(p);
17545
- const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
17717
+ const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
17546
17718
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
17547
17719
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
17548
17720
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -17558,7 +17730,7 @@ if (command === "passenger-sync-daemon") {
17558
17730
  for (const a of plan.actions) {
17559
17731
  if (a.kind === "enable") {
17560
17732
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
17561
- writeFileSync23(cfg.configFile, planEnable(readFileSync30(cfg.configFile, "utf8"), a.route));
17733
+ writeFileSync23(cfg.configFile, planEnable(readFileSync31(cfg.configFile, "utf8"), a.route));
17562
17734
  enabledSound.add(a.route);
17563
17735
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
17564
17736
  } else {
@@ -18559,7 +18731,7 @@ ${changed2.map((c) => ` - ${c}
18559
18731
  }
18560
18732
  let failed = false;
18561
18733
  for (const cmd2 of commands) {
18562
- const res2 = spawnSync6(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18734
+ const res2 = spawnSync7(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18563
18735
  if (res2.stdout) process.stdout.write(res2.stdout);
18564
18736
  if (res2.stderr) process.stderr.write(res2.stderr);
18565
18737
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -18577,7 +18749,7 @@ ${changed2.map((c) => ` - ${c}
18577
18749
  `);
18578
18750
  break;
18579
18751
  }
18580
- const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18752
+ const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18581
18753
  if (res.stdout) process.stdout.write(res.stdout);
18582
18754
  if (res.stderr) process.stderr.write(res.stderr);
18583
18755
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -18828,7 +19000,7 @@ ${changed2.map((c) => ` - ${c}
18828
19000
  `);
18829
19001
  if (!dryRun) {
18830
19002
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
18831
- const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync30(resolvedPath, "utf8")) : {};
19003
+ const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
18832
19004
  raw.agentSkills = result.agentSkills;
18833
19005
  mkdirSync22(dirOf(resolvedPath), { recursive: true });
18834
19006
  writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -18854,7 +19026,7 @@ ${changed2.map((c) => ` - ${c}
18854
19026
  throw e;
18855
19027
  }
18856
19028
  const oldName = cfg.ship.name;
18857
- writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync30(cfg.configFile, "utf8"), newName));
19029
+ writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), newName));
18858
19030
  cfg.ship.name = newName;
18859
19031
  let renameMachineId;
18860
19032
  try {
@@ -18897,11 +19069,37 @@ ${changed2.map((c) => ` - ${c}
18897
19069
  case "repos": {
18898
19070
  const sub = positional[1];
18899
19071
  const usage2 = () => {
18900
- process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
19072
+ process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos check [PATH | --route ROUTE [PATH] | ROUTE NAME] [--strict] [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
18901
19073
  process.exit(2);
18902
19074
  };
18903
- if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
18904
- const configText = readFileSync30(cfg.configFile, "utf8");
19075
+ if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
19076
+ const configText = readFileSync31(cfg.configFile, "utf8");
19077
+ if (sub === "check") {
19078
+ try {
19079
+ const routeArg = value("route") ?? positional[2];
19080
+ const found = cfg.routes.find((r) => r.route === routeArg);
19081
+ if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
19082
+ const named2 = value("route") ? void 0 : positional[3];
19083
+ const dir = named2 ? repoDirOf(configText, dirname20(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
19084
+ const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
19085
+ const o = target ? found.repoOverrides[target.name] : void 0;
19086
+ runReposCheck(dir, {
19087
+ hooks: { ...found.hooks, ...o?.hooks },
19088
+ labels: { ...found.labels, ...o?.labels },
19089
+ release: mergeRouteRelease(found.release, o?.release),
19090
+ branch: { ...found.branch, ...o?.branch },
19091
+ worktrees: { prefix: shipWorktreePrefixFor(found), nameOverride: o?.worktreeName }
19092
+ });
19093
+ } catch (e) {
19094
+ if (e instanceof ReposError) {
19095
+ process.stderr.write(`crew repos: ${e.message}
19096
+ `);
19097
+ process.exit(2);
19098
+ }
19099
+ throw e;
19100
+ }
19101
+ break;
19102
+ }
18905
19103
  if (sub === "hooks") {
18906
19104
  try {
18907
19105
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
@@ -18915,7 +19113,7 @@ ${changed2.map((c) => ` - ${c}
18915
19113
  }
18916
19114
  }, read: (r) => {
18917
19115
  try {
18918
- return readFileSync30(resolvePath(dir, r), "utf8");
19116
+ return readFileSync31(resolvePath(dir, r), "utf8");
18919
19117
  } catch {
18920
19118
  return void 0;
18921
19119
  }
@@ -19056,7 +19254,7 @@ ${changed2.map((c) => ` - ${c}
19056
19254
  let resolvedRemotes;
19057
19255
  if (existsSync34(resolvedFile)) {
19058
19256
  try {
19059
- resolvedRemotes = JSON.parse(readFileSync30(resolvedFile, "utf8"));
19257
+ resolvedRemotes = JSON.parse(readFileSync31(resolvedFile, "utf8"));
19060
19258
  } catch {
19061
19259
  }
19062
19260
  }
@@ -19077,7 +19275,7 @@ ${changed2.map((c) => ` - ${c}
19077
19275
  isCheckout,
19078
19276
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
19079
19277
  try {
19080
- return Object.keys(parseRepoConfig(readFileSync30(resolvePath(d, f), "utf8"), f).hooks).length > 0;
19278
+ return Object.keys(parseRepoConfig(readFileSync31(resolvePath(d, f), "utf8"), f).hooks).length > 0;
19081
19279
  } catch {
19082
19280
  return false;
19083
19281
  }
@@ -19186,7 +19384,7 @@ ${changed2.map((c) => ` - ${c}
19186
19384
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
19187
19385
  `);
19188
19386
  if (existsSync34(resolvedFile)) {
19189
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
19387
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
19190
19388
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
19191
19389
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
19192
19390
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -19245,7 +19443,7 @@ ${changed2.map((c) => ` - ${c}
19245
19443
  }
19246
19444
  for (const e of entries) {
19247
19445
  process.stdout.write(
19248
- `${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}
19446
+ `${e.id} ${e.started_at ?? "?"} ${e.outcome ?? "?"}${e.ticket_reference ? ` ${e.ticket_reference}` : ""}${e.model ? ` ${e.model}` : ""}${e.harness || e.provider ? ` (${[e.harness, e.provider].filter(Boolean).join(" \xB7 ")})` : ""}${e.num_turns != null ? ` turns ${e.num_turns}` : ""}${e.prefix_tokens != null ? ` prefix ${e.prefix_tokens}` : ""}
19249
19447
  `
19250
19448
  );
19251
19449
  }
@@ -19263,7 +19461,9 @@ ticket: ${entry.ticket_reference ?? "(none)"}
19263
19461
  model: ${entry.model ?? "?"}
19264
19462
  harness: ${entry.harness ?? "?"}
19265
19463
  provider: ${entry.provider ?? "?"}
19266
- `
19464
+ ` + (entry.num_turns != null ? `turns: ${entry.num_turns}
19465
+ ` : "") + (entry.prefix_tokens != null ? `prefix: ${entry.prefix_tokens} tokens
19466
+ ` : "")
19267
19467
  );
19268
19468
  if (flag("prompt")) {
19269
19469
  process.stdout.write(`
@@ -19544,7 +19744,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19544
19744
  process.exit(2);
19545
19745
  }
19546
19746
  if (found.shipNameAdopted && !dryRun) {
19547
- writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync30(cfg.configFile, "utf8"), found.shipNameAdopted));
19747
+ writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), found.shipNameAdopted));
19548
19748
  cfg.ship.name = found.shipNameAdopted;
19549
19749
  }
19550
19750
  if (found.shipRow) {
@@ -19590,7 +19790,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19590
19790
  const hadPreviousFile = existsSync34(resolvedPath);
19591
19791
  if (hadPreviousFile) {
19592
19792
  try {
19593
- previous = JSON.parse(readFileSync30(resolvedPath, "utf8"));
19793
+ previous = JSON.parse(readFileSync31(resolvedPath, "utf8"));
19594
19794
  } catch {
19595
19795
  }
19596
19796
  }
@@ -20475,7 +20675,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20475
20675
  };
20476
20676
  const droppedRoutes = [];
20477
20677
  try {
20478
- const rawCfg = parseYaml2(readFileSync30(cfg.configFile, "utf8"));
20678
+ const rawCfg = parseYaml2(readFileSync31(cfg.configFile, "utf8"));
20479
20679
  for (const r of rawCfg?.routes ?? []) {
20480
20680
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
20481
20681
  try {
@@ -20511,7 +20711,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20511
20711
  }
20512
20712
  const host = hostPlatform();
20513
20713
  const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
20514
- const unitStatus = control?.statusCommand ? spawnSync6(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
20714
+ const unitStatus = control?.statusCommand ? spawnSync7(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
20515
20715
  const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
20516
20716
  if (!unitRunning) {
20517
20717
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
@@ -20523,7 +20723,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20523
20723
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
20524
20724
  if (flag("now") && control?.stopCommand && control.startCommand) {
20525
20725
  for (const cmd of [control.stopCommand, control.startCommand]) {
20526
- const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
20726
+ const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
20527
20727
  if (cmd === control.startCommand && (res.error || res.status !== 0)) {
20528
20728
  process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
20529
20729
  `);
@@ -20569,7 +20769,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20569
20769
  }
20570
20770
  case "log": {
20571
20771
  try {
20572
- const text = readFileSync30(cfg.ship.logFile, "utf8").split("\n");
20772
+ const text = readFileSync31(cfg.ship.logFile, "utf8").split("\n");
20573
20773
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
20574
20774
  `);
20575
20775
  } catch {
@@ -20592,7 +20792,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20592
20792
  };
20593
20793
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
20594
20794
  const tracker = new Tracker(route, cfg.ship);
20595
- const configText = readFileSync30(cfg.configFile, "utf8");
20795
+ const configText = readFileSync31(cfg.configFile, "utf8");
20596
20796
  const cfgBase = dirname20(cfg.configFile);
20597
20797
  const configured = configuredRepos(configText, route.route, cfgBase);
20598
20798
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
@@ -20630,14 +20830,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20630
20830
  if (row) {
20631
20831
  await tracker.deleteRepoRow(row.id);
20632
20832
  if (existsSync34(resolvedFile)) {
20633
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
20833
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
20634
20834
  if (raw.repoNames) delete raw.repoNames[row.id];
20635
20835
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
20636
20836
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
20637
20837
  `);
20638
20838
  }
20639
20839
  }
20640
- if (existsSync34(join37(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
20840
+ if (existsSync34(join37(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
20641
20841
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
20642
20842
  break;
20643
20843
  }
@@ -20653,7 +20853,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20653
20853
  writeFileSync23(join37(dir, "README.md"), SAMPLE_README);
20654
20854
  writeFileSync23(join37(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
20655
20855
  const g = (...a) => {
20656
- const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
20856
+ const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
20657
20857
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
20658
20858
  };
20659
20859
  g("init", "-b", "main");
@@ -20670,7 +20870,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20670
20870
  });
20671
20871
  repoId = (await tracker.createRepoRow(payload)).id;
20672
20872
  if (existsSync34(resolvedFile)) {
20673
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
20873
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
20674
20874
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
20675
20875
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
20676
20876
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -20901,7 +21101,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20901
21101
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
20902
21102
  // config-provenance display; this is the field that actually
20903
21103
  // describes where a worktree lands by default.
20904
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename11(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
21104
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
20905
21105
  worktreeParent: resolvePath(r.dir, "..")
20906
21106
  })),
20907
21107
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -21068,7 +21268,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
21068
21268
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
21069
21269
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
21070
21270
  try {
21071
- return readFileSync30(p, "utf8");
21271
+ return readFileSync31(p, "utf8");
21072
21272
  } catch {
21073
21273
  return null;
21074
21274
  }
@@ -21111,7 +21311,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
21111
21311
  );
21112
21312
  for (const r of await resolvedRepos(route)) {
21113
21313
  const { config } = r;
21114
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename11(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
21314
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
21115
21315
  process.stdout.write(
21116
21316
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
21117
21317
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.77.0",
3
+ "version": "0.78.1",
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",