@tablation/crew 0.76.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +530 -205
  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
@@ -4853,6 +4853,158 @@ function renderEnvironment(i) {
4853
4853
  return lines.join("\n");
4854
4854
  }
4855
4855
 
4856
+ // src/contract.ts
4857
+ var DEFAULT_CONTRACT = {
4858
+ columns: {
4859
+ key: "issue_id",
4860
+ tag: "issue_tag",
4861
+ title: "title",
4862
+ status: "status",
4863
+ assignee: "assignee_id",
4864
+ priority: "priority",
4865
+ severity: "severity",
4866
+ slice: "project_id",
4867
+ repo: "repo_id",
4868
+ parent: "parent_id",
4869
+ blockedBy: "blocked_by",
4870
+ updatedAt: "updated_at",
4871
+ needsDesign: "needs_design",
4872
+ attachments: "attachments",
4873
+ workMode: "work_mode",
4874
+ reportType: "report_type",
4875
+ epic: "epic_id",
4876
+ author: "filed_by_id",
4877
+ ship: "ship_id",
4878
+ heldBy: "held_by_ship_id",
4879
+ resolutionNote: "resolution_note",
4880
+ verifiedSha: "verified_sha",
4881
+ prRef: "pr_ref",
4882
+ externalId: "external_id",
4883
+ position: "position",
4884
+ agentRuns: "agent_runs",
4885
+ tokensIn: "tokens_in",
4886
+ tokensOut: "tokens_out",
4887
+ cacheReadTokens: "cache_read_tokens",
4888
+ cacheWriteTokens: "cache_write_tokens",
4889
+ costUsd: "cost_usd"
4890
+ },
4891
+ comments: {
4892
+ parent: "ticket_id",
4893
+ body: "body",
4894
+ author: "team_member_id",
4895
+ createdAt: "created_at",
4896
+ ship: "ship_id",
4897
+ publicReply: "is_public_reply",
4898
+ authorName: "author_name"
4899
+ },
4900
+ statuses: {
4901
+ open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
4902
+ resolved: [
4903
+ "verified",
4904
+ "closed_deployed",
4905
+ "closed_wont_fix",
4906
+ "closed_duplicate",
4907
+ "closed_completed",
4908
+ "closed_cant_reproduce"
4909
+ ],
4910
+ approved: "accepted",
4911
+ parked: "blocked",
4912
+ building: "in_progress",
4913
+ handoff: "fixed",
4914
+ verifying: "qa",
4915
+ verified: "verified",
4916
+ deployed: "closed_deployed",
4917
+ needsHuman: "needs_info",
4918
+ reviewing: null,
4919
+ wontFix: "closed_wont_fix",
4920
+ draft: "draft"
4921
+ },
4922
+ epics: {
4923
+ statusColumn: "status",
4924
+ keyColumn: "epic_id",
4925
+ statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
4926
+ },
4927
+ priorityOrder: ["p0", "p1", "p2", "p3"],
4928
+ severityOrder: ["s1", "s2", "s3", "s4"],
4929
+ unknownPriorityRank: 2,
4930
+ coordinatingValue: "coordinating",
4931
+ manualWorkMode: "manual",
4932
+ hybridWorkMode: "hybrid",
4933
+ repoExemptReportTypes: ["coordinating", "question", "investigation"]
4934
+ };
4935
+ function closedStatuses(c) {
4936
+ return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
4937
+ }
4938
+ function resolveContract(override) {
4939
+ if (!override) return DEFAULT_CONTRACT;
4940
+ return {
4941
+ columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
4942
+ comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
4943
+ statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
4944
+ epics: {
4945
+ ...DEFAULT_CONTRACT.epics,
4946
+ ...override.epics,
4947
+ statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
4948
+ },
4949
+ priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
4950
+ severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
4951
+ unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
4952
+ coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
4953
+ manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
4954
+ hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
4955
+ // A workspace overriding `coordinatingValue` alone (renaming the rollup
4956
+ // marker) still gets that renamed value exempted here by default,
4957
+ // without also having to repeat it — only an explicit
4958
+ // `repoExemptReportTypes` overrides this derived default.
4959
+ repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
4960
+ };
4961
+ }
4962
+ function validateContract(c) {
4963
+ const problems = [];
4964
+ const open = new Set(c.statuses.open);
4965
+ const resolved = new Set(c.statuses.resolved);
4966
+ for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
4967
+ const v = c.statuses[role];
4968
+ if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
4969
+ }
4970
+ if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
4971
+ problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
4972
+ }
4973
+ if (!resolved.has(c.statuses.deployed)) {
4974
+ problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
4975
+ }
4976
+ if (resolved.has(c.statuses.handoff)) {
4977
+ problems.push(
4978
+ `statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
4979
+ );
4980
+ }
4981
+ if (!resolved.has(c.statuses.wontFix)) {
4982
+ problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
4983
+ }
4984
+ if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
4985
+ problems.push(
4986
+ `statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
4987
+ );
4988
+ }
4989
+ if (resolved.has(c.statuses.approved)) {
4990
+ problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
4991
+ }
4992
+ if (c.statuses.parked === c.statuses.approved) {
4993
+ problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
4994
+ }
4995
+ const epicValues = Object.values(c.epics.statuses);
4996
+ if (new Set(epicValues).size !== epicValues.length) {
4997
+ problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
4998
+ }
4999
+ if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
5000
+ if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
5001
+ for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
5002
+ if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
5003
+ }
5004
+ return problems;
5005
+ }
5006
+ var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
5007
+
4856
5008
  // src/agent-log.ts
4857
5009
  import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
4858
5010
  import { join as join12 } from "path";
@@ -4870,6 +5022,61 @@ var AgentLogHttpError = class extends Error {
4870
5022
  this.status = status2;
4871
5023
  }
4872
5024
  };
5025
+ async function request(target, method, path, body) {
5026
+ const url = `${target.baseUrl.replace(/\/+$/, "")}/api${path}`;
5027
+ const res = await fetch(url, {
5028
+ method,
5029
+ headers: {
5030
+ Authorization: `Bearer ${target.apiKey}`,
5031
+ "User-Agent": target.userAgent,
5032
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
5033
+ },
5034
+ body: body === void 0 ? void 0 : JSON.stringify(body)
5035
+ });
5036
+ if (!res.ok) throw new AgentLogHttpError(`${path}: ${res.status} ${res.statusText}`, res.status);
5037
+ return await res.json();
5038
+ }
5039
+ var issueColumns = /* @__PURE__ */ new Map();
5040
+ function columnsOf(target, modelId) {
5041
+ const key = `${target.baseUrl}|${target.workspaceId}|${modelId}`;
5042
+ let p = issueColumns.get(key);
5043
+ if (!p) {
5044
+ p = request(target, "GET", `/data-models/${modelId}`).then((m) => new Set((m.fields ?? []).map((f) => f.columnName)));
5045
+ p.catch(() => issueColumns.delete(key));
5046
+ issueColumns.set(key, p);
5047
+ }
5048
+ return p;
5049
+ }
5050
+ async function findTicketRecord(target, t, ref) {
5051
+ const filters = {
5052
+ combinator: "OR",
5053
+ conditions: [
5054
+ { columnName: t.keyColumn, operator: "EQ", value: ref },
5055
+ { columnName: t.tagColumn, operator: "EQ", value: ref }
5056
+ ]
5057
+ };
5058
+ const rows = await request(
5059
+ target,
5060
+ "GET",
5061
+ `/data-models/${t.issuesModelId}/records?limit=2&filters=${encodeURIComponent(JSON.stringify(filters))}`
5062
+ );
5063
+ return rows.length === 1 ? rows[0].id : void 0;
5064
+ }
5065
+ function counterIncrements(run2, cols, have) {
5066
+ const figures = [
5067
+ [cols.agentRuns, 1],
5068
+ [cols.tokensIn, run2.tokensIn],
5069
+ [cols.tokensOut, run2.tokensOut],
5070
+ [cols.cacheReadTokens, run2.cacheReadTokens],
5071
+ [cols.cacheWriteTokens, run2.cacheWriteTokens],
5072
+ [cols.costUsd, run2.costUsd]
5073
+ ];
5074
+ const out = {};
5075
+ for (const [column, value2] of figures) {
5076
+ if (value2 !== void 0 && Number.isFinite(value2) && have.has(column)) out[column] = { $op: "increment", value: value2 };
5077
+ }
5078
+ return out;
5079
+ }
4873
5080
  async function post(target, path, body) {
4874
5081
  const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
4875
5082
  const res = await fetch(url, {
@@ -4886,6 +5093,13 @@ async function post(target, path, body) {
4886
5093
  return await res.json();
4887
5094
  }
4888
5095
  async function reportAgentRun(target, run2) {
5096
+ let ticketRecordId;
5097
+ if (target.tickets && run2.ticketReference) {
5098
+ try {
5099
+ ticketRecordId = await findTicketRecord(target, target.tickets, run2.ticketReference);
5100
+ } catch {
5101
+ }
5102
+ }
4889
5103
  const body = {
4890
5104
  agentId: target.agentId,
4891
5105
  ticketReference: run2.ticketReference,
@@ -4903,13 +5117,14 @@ async function reportAgentRun(target, run2) {
4903
5117
  promptVersion: run2.promptVersion,
4904
5118
  promptSha: run2.promptSha
4905
5119
  };
5120
+ const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
4906
5121
  let entry;
4907
5122
  try {
4908
- entry = await post(target, "/agents/log", { ...body, harness: run2.harness, provider: run2.provider });
5123
+ entry = await post(target, "/agents/log", { ...body, ...workItem, harness: run2.harness, provider: run2.provider });
4909
5124
  } catch (e) {
4910
5125
  if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
4911
5126
  if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
4912
- if (!(e instanceof AgentLogHttpError) || e.status !== 400 || !run2.harness && !run2.provider) throw e;
5127
+ if (e.status !== 400 || !run2.harness && !run2.provider && !ticketRecordId) throw e;
4913
5128
  entry = await post(target, "/agents/log", body);
4914
5129
  }
4915
5130
  for (const cycle of run2.cycles) {
@@ -4920,7 +5135,37 @@ async function reportAgentRun(target, run2) {
4920
5135
  ...cycle.role ? { role: cycle.role } : {}
4921
5136
  });
4922
5137
  }
4923
- return { agentLogId: entry.id };
5138
+ const counters = target.tickets && ticketRecordId ? await addRunToTicket(target, target.tickets, ticketRecordId, run2) : void 0;
5139
+ return {
5140
+ agentLogId: entry.id,
5141
+ ...counters?.warning ? { counterWarning: counters.warning } : {},
5142
+ ...counters?.retry ? { counterRetry: counters.retry } : {}
5143
+ };
5144
+ }
5145
+ var isRetryable = (e) => !(e instanceof AgentLogHttpError) || e.status >= 500;
5146
+ async function writeCounters(target, w) {
5147
+ const have = await columnsOf(target, w.issuesModelId);
5148
+ const body = Object.fromEntries(Object.entries(w.body).filter(([column]) => have.has(column)));
5149
+ if (Object.keys(body).length === 0) return;
5150
+ await request(target, "PATCH", `/data-models/${w.issuesModelId}/records/${w.recordId}`, body);
5151
+ }
5152
+ async function addRunToTicket(target, t, recordId, run2) {
5153
+ const write = {
5154
+ issuesModelId: t.issuesModelId,
5155
+ recordId,
5156
+ ticketReference: run2.ticketReference,
5157
+ counters: t.counters,
5158
+ body: counterIncrements(run2, t.counters, new Set(Object.values(t.counters)))
5159
+ };
5160
+ try {
5161
+ await writeCounters(target, write);
5162
+ return {};
5163
+ } catch (e) {
5164
+ return {
5165
+ warning: `could not add run spend to ${run2.ticketReference}: ${e.message}`,
5166
+ ...isRetryable(e) ? { retry: write } : {}
5167
+ };
5168
+ }
4924
5169
  }
4925
5170
  var queueDir = (stateDir) => join12(stateDir, "pending-agent-logs");
4926
5171
  function queueAgentRun(stateDir, name, q) {
@@ -4943,7 +5188,17 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
4943
5188
  try {
4944
5189
  const apiKey = await keyFor(q.role);
4945
5190
  if (!apiKey) continue;
4946
- await reportAgentRun({ ...q.target, apiKey }, q.report);
5191
+ const sentRun = await reportAgentRun({ ...q.target, apiKey }, q.report);
5192
+ if (sentRun.counterWarning) log.warn(sentRun.counterWarning);
5193
+ if (sentRun.counterRetry) queueTicketCounters(stateDir, f.replace(/\.json$/, ""), {
5194
+ version: 1,
5195
+ route: q.route,
5196
+ role: q.role,
5197
+ target: q.target,
5198
+ write: sentRun.counterRetry,
5199
+ attempts: 0,
5200
+ nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
5201
+ });
4947
5202
  rmSync6(path, { force: true });
4948
5203
  sent += 1;
4949
5204
  log.emit(`sent a queued Agent Log report (${f})`);
@@ -4965,6 +5220,49 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
4965
5220
  }
4966
5221
  return sent;
4967
5222
  }
5223
+ var countersDir = (stateDir) => join12(stateDir, "pending-ticket-counters");
5224
+ function queueTicketCounters(stateDir, name, q) {
5225
+ mkdirSync6(countersDir(stateDir), { recursive: true });
5226
+ writeFileSync7(join12(countersDir(stateDir), `${name}.json`), JSON.stringify(q));
5227
+ }
5228
+ async function replayQueuedTicketCounters(stateDir, route, keyFor, log, now = /* @__PURE__ */ new Date()) {
5229
+ const dir = countersDir(stateDir);
5230
+ if (!existsSync11(dir)) return 0;
5231
+ let sent = 0;
5232
+ for (const f of readdirSync4(dir).filter((n) => n.endsWith(".json")).sort()) {
5233
+ const path = join12(dir, f);
5234
+ let q;
5235
+ try {
5236
+ q = JSON.parse(readFileSync11(path, "utf8"));
5237
+ } catch {
5238
+ continue;
5239
+ }
5240
+ if (q.version !== 1 || q.route !== route || now.toISOString() < q.nextAttemptAt) continue;
5241
+ try {
5242
+ const apiKey = await keyFor(q.role);
5243
+ if (!apiKey) continue;
5244
+ await writeCounters({ ...q.target, apiKey }, q.write);
5245
+ rmSync6(path, { force: true });
5246
+ sent += 1;
5247
+ log.emit(`added a queued run's spend to ${q.write.ticketReference ?? q.write.recordId} (${f})`);
5248
+ } catch (e) {
5249
+ if (!isRetryable(e)) {
5250
+ rmSync6(path, { force: true });
5251
+ log.warn(`dropped a queued ticket counter update (${f}): ${e.message}`);
5252
+ continue;
5253
+ }
5254
+ const attempts = q.attempts + 1;
5255
+ writeFileSync7(path, JSON.stringify({
5256
+ ...q,
5257
+ attempts,
5258
+ warned: true,
5259
+ nextAttemptAt: new Date(now.getTime() + sleepBackoff(attempts)).toISOString()
5260
+ }));
5261
+ if (!q.warned) log.warn(`a queued ticket counter update could not be sent yet (${e.message}); will retry`);
5262
+ }
5263
+ }
5264
+ return sent;
5265
+ }
4968
5266
 
4969
5267
  // src/ticket-attribution.ts
4970
5268
  var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
@@ -5145,152 +5443,6 @@ import { basename as basename6 } from "path";
5145
5443
  import { hostname } from "os";
5146
5444
  import { TablationClient } from "@tablation/client";
5147
5445
 
5148
- // src/contract.ts
5149
- var DEFAULT_CONTRACT = {
5150
- columns: {
5151
- key: "issue_id",
5152
- tag: "issue_tag",
5153
- title: "title",
5154
- status: "status",
5155
- assignee: "assignee_id",
5156
- priority: "priority",
5157
- severity: "severity",
5158
- slice: "project_id",
5159
- repo: "repo_id",
5160
- parent: "parent_id",
5161
- blockedBy: "blocked_by",
5162
- updatedAt: "updated_at",
5163
- needsDesign: "needs_design",
5164
- attachments: "attachments",
5165
- workMode: "work_mode",
5166
- reportType: "report_type",
5167
- epic: "epic_id",
5168
- author: "filed_by_id",
5169
- ship: "ship_id",
5170
- heldBy: "held_by_ship_id",
5171
- resolutionNote: "resolution_note",
5172
- verifiedSha: "verified_sha",
5173
- prRef: "pr_ref",
5174
- externalId: "external_id",
5175
- position: "position"
5176
- },
5177
- comments: {
5178
- parent: "ticket_id",
5179
- body: "body",
5180
- author: "team_member_id",
5181
- createdAt: "created_at",
5182
- ship: "ship_id",
5183
- publicReply: "is_public_reply",
5184
- authorName: "author_name"
5185
- },
5186
- statuses: {
5187
- open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
5188
- resolved: [
5189
- "verified",
5190
- "closed_deployed",
5191
- "closed_wont_fix",
5192
- "closed_duplicate",
5193
- "closed_completed",
5194
- "closed_cant_reproduce"
5195
- ],
5196
- approved: "accepted",
5197
- parked: "blocked",
5198
- building: "in_progress",
5199
- handoff: "fixed",
5200
- verifying: "qa",
5201
- verified: "verified",
5202
- deployed: "closed_deployed",
5203
- needsHuman: "needs_info",
5204
- reviewing: null,
5205
- wontFix: "closed_wont_fix",
5206
- draft: "draft"
5207
- },
5208
- epics: {
5209
- statusColumn: "status",
5210
- keyColumn: "epic_id",
5211
- statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
5212
- },
5213
- priorityOrder: ["p0", "p1", "p2", "p3"],
5214
- severityOrder: ["s1", "s2", "s3", "s4"],
5215
- unknownPriorityRank: 2,
5216
- coordinatingValue: "coordinating",
5217
- manualWorkMode: "manual",
5218
- hybridWorkMode: "hybrid",
5219
- repoExemptReportTypes: ["coordinating", "question", "investigation"]
5220
- };
5221
- function closedStatuses(c) {
5222
- return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
5223
- }
5224
- function resolveContract(override) {
5225
- if (!override) return DEFAULT_CONTRACT;
5226
- return {
5227
- columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
5228
- comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
5229
- statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
5230
- epics: {
5231
- ...DEFAULT_CONTRACT.epics,
5232
- ...override.epics,
5233
- statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
5234
- },
5235
- priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
5236
- severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
5237
- unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
5238
- coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
5239
- manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
5240
- hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
5241
- // A workspace overriding `coordinatingValue` alone (renaming the rollup
5242
- // marker) still gets that renamed value exempted here by default,
5243
- // without also having to repeat it — only an explicit
5244
- // `repoExemptReportTypes` overrides this derived default.
5245
- repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
5246
- };
5247
- }
5248
- function validateContract(c) {
5249
- const problems = [];
5250
- const open = new Set(c.statuses.open);
5251
- const resolved = new Set(c.statuses.resolved);
5252
- for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
5253
- const v = c.statuses[role];
5254
- if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
5255
- }
5256
- if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
5257
- problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
5258
- }
5259
- if (!resolved.has(c.statuses.deployed)) {
5260
- problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
5261
- }
5262
- if (resolved.has(c.statuses.handoff)) {
5263
- problems.push(
5264
- `statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
5265
- );
5266
- }
5267
- if (!resolved.has(c.statuses.wontFix)) {
5268
- problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
5269
- }
5270
- if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
5271
- problems.push(
5272
- `statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
5273
- );
5274
- }
5275
- if (resolved.has(c.statuses.approved)) {
5276
- problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
5277
- }
5278
- if (c.statuses.parked === c.statuses.approved) {
5279
- problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
5280
- }
5281
- const epicValues = Object.values(c.epics.statuses);
5282
- if (new Set(epicValues).size !== epicValues.length) {
5283
- problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
5284
- }
5285
- if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
5286
- if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
5287
- for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
5288
- if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
5289
- }
5290
- return problems;
5291
- }
5292
- var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
5293
-
5294
5446
  // src/state.ts
5295
5447
  import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4 } from "fs";
5296
5448
  import { join as join13 } from "path";
@@ -8144,12 +8296,26 @@ function planAgentRun(o) {
8144
8296
  // resolved yet has nothing to authenticate the MCP connection with either.
8145
8297
  mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
8146
8298
  });
8299
+ const columns = resolveContract(o.route.contract).columns;
8147
8300
  const agentLog = o.route.resolved && o.apiKey ? {
8148
8301
  baseUrl: o.route.baseUrl,
8149
8302
  workspaceId: o.route.resolved.workspaceId,
8150
8303
  apiKey: o.apiKey,
8151
8304
  userAgent: o.ship.userAgent,
8152
- agentId: o.resolvedAgentId
8305
+ agentId: o.resolvedAgentId,
8306
+ ...o.route.resolved.models?.issues ? { tickets: {
8307
+ issuesModelId: o.route.resolved.models.issues,
8308
+ keyColumn: columns.key,
8309
+ tagColumn: columns.tag,
8310
+ counters: {
8311
+ agentRuns: columns.agentRuns,
8312
+ tokensIn: columns.tokensIn,
8313
+ tokensOut: columns.tokensOut,
8314
+ cacheReadTokens: columns.cacheReadTokens,
8315
+ cacheWriteTokens: columns.cacheWriteTokens,
8316
+ costUsd: columns.costUsd
8317
+ }
8318
+ } } : {}
8153
8319
  } : void 0;
8154
8320
  return {
8155
8321
  role: o.role,
@@ -8519,7 +8685,22 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
8519
8685
  promptSha: id.promptSha
8520
8686
  };
8521
8687
  try {
8522
- await reportAgentRun({ ...id.agentLog, apiKey: agentLogKey }, report);
8688
+ const sent = await reportAgentRun({ ...id.agentLog, apiKey: agentLogKey }, report);
8689
+ if (sent.counterWarning) emit.warn(sent.counterWarning, { step: "agent" });
8690
+ if (sent.counterRetry) {
8691
+ try {
8692
+ queueTicketCounters(stateDirOfStream(id.streamPath), basename7(id.streamPath, ".jsonl"), {
8693
+ version: 1,
8694
+ route: id.route,
8695
+ role: id.role,
8696
+ target: id.agentLog,
8697
+ write: sent.counterRetry,
8698
+ attempts: 0,
8699
+ nextAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
8700
+ });
8701
+ } catch {
8702
+ }
8703
+ }
8523
8704
  } catch (e) {
8524
8705
  if (e instanceof AgentLogUnreachableError) {
8525
8706
  try {
@@ -8860,10 +9041,10 @@ function doctorSelftestLines(stored, toolchains) {
8860
9041
  }
8861
9042
 
8862
9043
  // src/cli.ts
8863
- import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename11 } from "path";
9044
+ import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename12 } from "path";
8864
9045
  import { fileURLToPath } from "url";
8865
9046
  import { hostname as hostname2, homedir as homedir10 } from "os";
8866
- import { spawnSync as spawnSync6 } from "child_process";
9047
+ import { spawnSync as spawnSync7 } from "child_process";
8867
9048
  import { parse as parseYaml2 } from "yaml";
8868
9049
 
8869
9050
  // src/argv.ts
@@ -14212,12 +14393,107 @@ async function showLogEntry(route, entryId, opts = {}) {
14212
14393
  };
14213
14394
  }
14214
14395
 
14396
+ // src/repos-check.ts
14397
+ import { spawnSync as spawnSync6 } from "child_process";
14398
+ import { readFileSync as readFileSync24 } from "fs";
14399
+ import { basename as basename8 } from "path";
14400
+ var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
14401
+ function checkHookSyntax(script, shell) {
14402
+ const spec = resolveShell(shell);
14403
+ const name = basename8(spec.bin).replace(/\.exe$/i, "").toLowerCase();
14404
+ let run2;
14405
+ if (POSIX_SHELL.test(name)) {
14406
+ run2 = spawnSync6(spec.bin, ["-n"], { input: script, encoding: "utf8" });
14407
+ } else if (/^(pwsh|powershell)$/.test(name)) {
14408
+ 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}";
14409
+ run2 = spawnSync6(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
14410
+ encoding: "utf8",
14411
+ env: { ...process.env, CREW_CHECK_SCRIPT: script }
14412
+ });
14413
+ } else {
14414
+ return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
14415
+ }
14416
+ if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
14417
+ if (run2.status === 0) return { status: "ok" };
14418
+ const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
14419
+ return { status: "error", message };
14420
+ }
14421
+ function renderSamples(cfg, dir) {
14422
+ const checkout = basename8(dir.replace(/[/\\]+$/, ""));
14423
+ const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
14424
+ const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
14425
+ return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
14426
+ const branch = renderBranchName(effectiveBranchTemplate(cfg, ctx.prefix), ctx);
14427
+ const worktree = effectiveWorktreeDirName(cfg, dir, branch, "123", (t) => renderWorktreeName(t, { ...ctx, dir: checkout }));
14428
+ return { case: label, branch, worktree };
14429
+ });
14430
+ }
14431
+ function checkRepoSpec(dir, opts = {}) {
14432
+ const empty = { parsed: null, effective: null, readiness: [], hooks: [], shadowed: [], samples: [] };
14433
+ const file = findRepoConfig(dir);
14434
+ if (!file) {
14435
+ return { ...empty, file: null, error: `no .crew.yaml, .crew.yml or .crew.json in ${dir}`, exitCode: 1 };
14436
+ }
14437
+ let parsed;
14438
+ try {
14439
+ parsed = parseRepoConfig(readFileSync24(file, "utf8"), file);
14440
+ } catch (e) {
14441
+ if (!(e instanceof RepoConfigError)) throw e;
14442
+ return { ...empty, file, error: e.message, exitCode: 1 };
14443
+ }
14444
+ const effective = resolveRepoConfig(parsed, opts.ship, dir);
14445
+ const readiness = validateEffective(effective);
14446
+ const hooks = Object.keys(effective.hooks).map((name) => ({
14447
+ name,
14448
+ label: hookLabel(effective, name),
14449
+ syntax: checkHookSyntax(effective.hooks[name], effective.shell)
14450
+ }));
14451
+ const failed = hooks.some((h) => h.syntax.status === "error");
14452
+ return {
14453
+ file,
14454
+ parsed,
14455
+ effective,
14456
+ readiness,
14457
+ hooks,
14458
+ shadowed: effective.shadowed,
14459
+ samples: renderSamples(effective, dir),
14460
+ error: null,
14461
+ exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
14462
+ };
14463
+ }
14464
+ function renderRepoCheck(r) {
14465
+ if (r.error) return `${r.file ?? "crew repos check"}: ${r.error}
14466
+ `;
14467
+ const e = r.effective;
14468
+ const lines = [
14469
+ `${r.file}`,
14470
+ ` platform ${e.platform}`,
14471
+ ` release.mode ${e.release.mode}`,
14472
+ ` release.versioning ${e.release.versioning}`,
14473
+ ` branch.base ${e.branch.base}`,
14474
+ ` branch.name ${e.branch.name}`,
14475
+ ` branch.push ${e.branch.push}`
14476
+ ];
14477
+ for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
14478
+ lines.push(r.hooks.length ? " hooks" : " hooks none");
14479
+ for (const h of r.hooks) {
14480
+ const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
14481
+ const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
14482
+ lines.push(` ${h.name}${named}: ${syntax}`);
14483
+ if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
14484
+ }
14485
+ if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
14486
+ for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
14487
+ return `${lines.join("\n")}
14488
+ `;
14489
+ }
14490
+
14215
14491
  // src/repos-hooks.ts
14216
14492
  import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
14217
14493
 
14218
14494
  // src/repos-cmd.ts
14219
14495
  import { homedir as homedir9 } from "os";
14220
- import { basename as basename8, join as join28, resolve as resolve3 } from "path";
14496
+ import { basename as basename9, join as join28, resolve as resolve3 } from "path";
14221
14497
  import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
14222
14498
  var ReposError = class extends Error {
14223
14499
  };
@@ -14289,7 +14565,7 @@ function configuredRepos(text, route, base) {
14289
14565
  });
14290
14566
  }
14291
14567
  const dir = m.get("dir");
14292
- return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
14568
+ return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
14293
14569
  }
14294
14570
  function cloneBaseOf(text, route, base, isCheckout) {
14295
14571
  const m = routeMapOf(parseDocument2(text), route);
@@ -14378,7 +14654,7 @@ function planRepoAdd(i) {
14378
14654
  throw new ReposError(`${target} is already configured on route ${i.route}`);
14379
14655
  }
14380
14656
  const matched = matchRepoName(i.origin, i.resolved);
14381
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
14657
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
14382
14658
  let warning;
14383
14659
  if (!matched) {
14384
14660
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -14894,7 +15170,7 @@ ${run2.err}` });
14894
15170
  }
14895
15171
 
14896
15172
  // src/sample-ticket.ts
14897
- import { basename as basename9, dirname as dirname17, join as join29 } from "path";
15173
+ import { basename as basename10, dirname as dirname17, join as join29 } from "path";
14898
15174
  var SAMPLE_REPO = "crew-sample";
14899
15175
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
14900
15176
  var SAMPLE_MARKER = "sample: true";
@@ -15010,8 +15286,8 @@ function schedulerInstalled(plan, exists, crontab) {
15010
15286
  }
15011
15287
 
15012
15288
  // src/worktree-sweep.ts
15013
- import { existsSync as existsSync27, readFileSync as readFileSync24, readdirSync as readdirSync8 } from "fs";
15014
- import { basename as basename10, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
15289
+ import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync8 } from "fs";
15290
+ import { basename as basename11, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
15015
15291
  function planWorktreeSweep(target, tickets, contract) {
15016
15292
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
15017
15293
  const verified = contract.statuses.verified;
@@ -15042,7 +15318,7 @@ function ticketForWorktree(w, byNumber, contract) {
15042
15318
  const t = num ? byNumber.get(num) : void 0;
15043
15319
  if (t && ticketStems(t).has(stem)) return t;
15044
15320
  }
15045
- const dir = basename10(w.path);
15321
+ const dir = basename11(w.path);
15046
15322
  const m = DIR_NUMBER.exec(dir);
15047
15323
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
15048
15324
  if (!w.branch) {
@@ -15058,7 +15334,7 @@ function worktreeTicketNumbers(target) {
15058
15334
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
15059
15335
  if (n) nums.add(n);
15060
15336
  }
15061
- const m = DIR_NUMBER_SUFFIXED.exec(basename10(w.path));
15337
+ const m = DIR_NUMBER_SUFFIXED.exec(basename11(w.path));
15062
15338
  if (m) nums.add(m[1]);
15063
15339
  }
15064
15340
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
@@ -15066,7 +15342,7 @@ function worktreeTicketNumbers(target) {
15066
15342
  }
15067
15343
  function siblingWorktreeDirs(target) {
15068
15344
  const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
15069
- const stem = `${basename10(target.dir.replace(/[/\\]+$/, ""))}-`;
15345
+ const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
15070
15346
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
15071
15347
  let names;
15072
15348
  try {
@@ -15091,7 +15367,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
15091
15367
  }
15092
15368
  function warnOrphanedWorktreeDirs(paths, log, held) {
15093
15369
  for (const p of paths) {
15094
- const number = DIR_NUMBER_SUFFIXED.exec(basename10(p))?.[1];
15370
+ const number = DIR_NUMBER_SUFFIXED.exec(basename11(p))?.[1];
15095
15371
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
15096
15372
  if (when) {
15097
15373
  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" });
@@ -15181,7 +15457,7 @@ function isHandBackMerge(path, base) {
15181
15457
  if (!msgPath) return false;
15182
15458
  let first;
15183
15459
  try {
15184
- first = readFileSync24(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
15460
+ first = readFileSync25(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
15185
15461
  } catch {
15186
15462
  return false;
15187
15463
  }
@@ -15235,7 +15511,7 @@ function isTicketBranch(name, base, stems) {
15235
15511
  }
15236
15512
  function sweepCandidatesExist(cwd, base) {
15237
15513
  const linked = worktrees(cwd).slice(1);
15238
- if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename10(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
15514
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename11(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
15239
15515
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
15240
15516
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
15241
15517
  }
@@ -15331,13 +15607,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
15331
15607
  }
15332
15608
 
15333
15609
  // src/cli.ts
15334
- 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";
15610
+ 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";
15335
15611
  import { createInterface } from "readline/promises";
15336
15612
  import { dirname as dirOf, resolve as resolvePath } from "path";
15337
15613
 
15338
15614
  // src/ssh-keys.ts
15339
15615
  import { execFileSync as execFileSync6 } from "child_process";
15340
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, chmodSync } from "fs";
15616
+ import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync26, chmodSync } from "fs";
15341
15617
  import { dirname as dirname19, join as join32 } from "path";
15342
15618
  var SshKeyError = class extends Error {
15343
15619
  };
@@ -15371,7 +15647,7 @@ function ensureShipSshKeypair(stateDir) {
15371
15647
  if (!existsSync29(publicKeyPath)) {
15372
15648
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
15373
15649
  }
15374
- const publicKey = readFileSync25(publicKeyPath, "utf8").trim();
15650
+ const publicKey = readFileSync26(publicKeyPath, "utf8").trim();
15375
15651
  return { privateKeyPath, publicKeyPath, publicKey };
15376
15652
  }
15377
15653
  function sshKeygenAvailable() {
@@ -15588,7 +15864,7 @@ function syncAllPassengerCheckouts(cfg) {
15588
15864
 
15589
15865
  // src/tunnel.ts
15590
15866
  import { spawn as spawn5 } from "child_process";
15591
- import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15867
+ import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync27, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15592
15868
  import { randomInt } from "crypto";
15593
15869
  import { join as join33 } from "path";
15594
15870
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -15663,7 +15939,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
15663
15939
  const path = tunnelStatePath(stateDir, workspaceId);
15664
15940
  if (!existsSync30(path)) return void 0;
15665
15941
  try {
15666
- return JSON.parse(readFileSync26(path, "utf8"));
15942
+ return JSON.parse(readFileSync27(path, "utf8"));
15667
15943
  } catch {
15668
15944
  return void 0;
15669
15945
  }
@@ -15890,7 +16166,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
15890
16166
  import { createServer as createHttpServer } from "http";
15891
16167
  import { spawn as spawn6 } from "child_process";
15892
16168
  import { randomBytes } from "crypto";
15893
- import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync27, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
16169
+ import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
15894
16170
  import { join as join34 } from "path";
15895
16171
 
15896
16172
  // src/secret-compare.ts
@@ -15986,7 +16262,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
15986
16262
  const path = syncDaemonStatePath(stateDir, workspaceId);
15987
16263
  if (!existsSync31(path)) return void 0;
15988
16264
  try {
15989
- return JSON.parse(readFileSync27(path, "utf8"));
16265
+ return JSON.parse(readFileSync28(path, "utf8"));
15990
16266
  } catch {
15991
16267
  return void 0;
15992
16268
  }
@@ -16103,11 +16379,11 @@ function describeCause(err) {
16103
16379
  }
16104
16380
 
16105
16381
  // src/daemon.ts
16106
- import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync29, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
16382
+ import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
16107
16383
  import { join as join36 } from "path";
16108
16384
 
16109
16385
  // src/interrupted.ts
16110
- import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync28 } from "fs";
16386
+ import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync29 } from "fs";
16111
16387
  import { join as join35 } from "path";
16112
16388
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
16113
16389
  function interruptedComment(b) {
@@ -16129,7 +16405,7 @@ function runFinished(streamPath) {
16129
16405
  const path = `${streamPath}.meta.json`;
16130
16406
  if (!existsSync32(path)) return false;
16131
16407
  try {
16132
- return JSON.parse(readFileSync28(path, "utf8")).running !== true;
16408
+ return JSON.parse(readFileSync29(path, "utf8")).running !== true;
16133
16409
  } catch {
16134
16410
  return true;
16135
16411
  }
@@ -16286,7 +16562,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
16286
16562
  }
16287
16563
  function readDaemonStarted(stateDir) {
16288
16564
  try {
16289
- const v = JSON.parse(readFileSync29(daemonStampFor(stateDir), "utf8")).startedAt;
16565
+ const v = JSON.parse(readFileSync30(daemonStampFor(stateDir), "utf8")).startedAt;
16290
16566
  return typeof v === "number" ? v : null;
16291
16567
  } catch {
16292
16568
  return null;
@@ -16467,6 +16743,12 @@ async function replayPendingWrites(ctx) {
16467
16743
  async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
16468
16744
  emit
16469
16745
  );
16746
+ await replayQueuedTicketCounters(
16747
+ ship.stateDir,
16748
+ routeSlug(route.route),
16749
+ async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
16750
+ emit
16751
+ );
16470
16752
  } catch (e) {
16471
16753
  emit.warn(`could not replay pending writes: ${e.message}`, { step: "agent" });
16472
16754
  }
@@ -16831,6 +17113,7 @@ function usage(problem, requested = false) {
16831
17113
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
16832
17114
  crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
16833
17115
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
17116
+ crew repos check [PATH] validate a checkout's .crew.yaml without attaching it (--route ROUTE, --strict, --json)
16834
17117
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
16835
17118
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
16836
17119
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches (--json: every Repos row too)
@@ -16929,6 +17212,22 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
16929
17212
  "worktree"
16930
17213
  ]);
16931
17214
  if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
17215
+ function runReposCheck(dir, ship) {
17216
+ const report = checkRepoSpec(dir, { ship, strict: flag("strict") });
17217
+ if (flag("json")) {
17218
+ const { exitCode: _exit, ...rest } = report;
17219
+ process.stdout.write(`${JSON.stringify(rest)}
17220
+ `);
17221
+ } else {
17222
+ process.stdout.write(renderRepoCheck(report));
17223
+ }
17224
+ process.exitCode = report.exitCode;
17225
+ }
17226
+ var reposCheckBare = command === "repos" && positional[1] === "check" && !value("route") && positional.length < 4;
17227
+ if (reposCheckBare) {
17228
+ runReposCheck(resolvePath(positional[2] ?? "."));
17229
+ process.exit(process.exitCode ?? 0);
17230
+ }
16932
17231
  function findSyncRoute(c, name) {
16933
17232
  if (!name && c.routes.length > 1) {
16934
17233
  throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
@@ -17315,7 +17614,7 @@ if (command === "passenger-sync-daemon") {
17315
17614
  `
17316
17615
  );
17317
17616
  if (!dryRun) {
17318
- spawnSync6("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
17617
+ spawnSync7("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
17319
17618
  try {
17320
17619
  unlinkSync5(staleTimer);
17321
17620
  } catch {
@@ -17355,7 +17654,7 @@ if (command === "passenger-sync-daemon") {
17355
17654
  async function doctorFix(o) {
17356
17655
  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))));
17357
17656
  const unitExists = (p) => existsSync34(p);
17358
- const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
17657
+ const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
17359
17658
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
17360
17659
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
17361
17660
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -17371,7 +17670,7 @@ if (command === "passenger-sync-daemon") {
17371
17670
  for (const a of plan.actions) {
17372
17671
  if (a.kind === "enable") {
17373
17672
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
17374
- writeFileSync23(cfg.configFile, planEnable(readFileSync30(cfg.configFile, "utf8"), a.route));
17673
+ writeFileSync23(cfg.configFile, planEnable(readFileSync31(cfg.configFile, "utf8"), a.route));
17375
17674
  enabledSound.add(a.route);
17376
17675
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
17377
17676
  } else {
@@ -18372,7 +18671,7 @@ ${changed2.map((c) => ` - ${c}
18372
18671
  }
18373
18672
  let failed = false;
18374
18673
  for (const cmd2 of commands) {
18375
- const res2 = spawnSync6(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18674
+ const res2 = spawnSync7(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18376
18675
  if (res2.stdout) process.stdout.write(res2.stdout);
18377
18676
  if (res2.stderr) process.stderr.write(res2.stderr);
18378
18677
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -18390,7 +18689,7 @@ ${changed2.map((c) => ` - ${c}
18390
18689
  `);
18391
18690
  break;
18392
18691
  }
18393
- const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18692
+ const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18394
18693
  if (res.stdout) process.stdout.write(res.stdout);
18395
18694
  if (res.stderr) process.stderr.write(res.stderr);
18396
18695
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -18641,7 +18940,7 @@ ${changed2.map((c) => ` - ${c}
18641
18940
  `);
18642
18941
  if (!dryRun) {
18643
18942
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
18644
- const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync30(resolvedPath, "utf8")) : {};
18943
+ const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
18645
18944
  raw.agentSkills = result.agentSkills;
18646
18945
  mkdirSync22(dirOf(resolvedPath), { recursive: true });
18647
18946
  writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -18667,7 +18966,7 @@ ${changed2.map((c) => ` - ${c}
18667
18966
  throw e;
18668
18967
  }
18669
18968
  const oldName = cfg.ship.name;
18670
- writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync30(cfg.configFile, "utf8"), newName));
18969
+ writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), newName));
18671
18970
  cfg.ship.name = newName;
18672
18971
  let renameMachineId;
18673
18972
  try {
@@ -18710,11 +19009,37 @@ ${changed2.map((c) => ` - ${c}
18710
19009
  case "repos": {
18711
19010
  const sub = positional[1];
18712
19011
  const usage2 = () => {
18713
- 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");
19012
+ 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");
18714
19013
  process.exit(2);
18715
19014
  };
18716
- if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
18717
- const configText = readFileSync30(cfg.configFile, "utf8");
19015
+ if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
19016
+ const configText = readFileSync31(cfg.configFile, "utf8");
19017
+ if (sub === "check") {
19018
+ try {
19019
+ const routeArg = value("route") ?? positional[2];
19020
+ const found = cfg.routes.find((r) => r.route === routeArg);
19021
+ if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
19022
+ const named2 = value("route") ? void 0 : positional[3];
19023
+ const dir = named2 ? repoDirOf(configText, dirname20(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
19024
+ const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
19025
+ const o = target ? found.repoOverrides[target.name] : void 0;
19026
+ runReposCheck(dir, {
19027
+ hooks: { ...found.hooks, ...o?.hooks },
19028
+ labels: { ...found.labels, ...o?.labels },
19029
+ release: mergeRouteRelease(found.release, o?.release),
19030
+ branch: { ...found.branch, ...o?.branch },
19031
+ worktrees: { prefix: shipWorktreePrefixFor(found), nameOverride: o?.worktreeName }
19032
+ });
19033
+ } catch (e) {
19034
+ if (e instanceof ReposError) {
19035
+ process.stderr.write(`crew repos: ${e.message}
19036
+ `);
19037
+ process.exit(2);
19038
+ }
19039
+ throw e;
19040
+ }
19041
+ break;
19042
+ }
18718
19043
  if (sub === "hooks") {
18719
19044
  try {
18720
19045
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
@@ -18728,7 +19053,7 @@ ${changed2.map((c) => ` - ${c}
18728
19053
  }
18729
19054
  }, read: (r) => {
18730
19055
  try {
18731
- return readFileSync30(resolvePath(dir, r), "utf8");
19056
+ return readFileSync31(resolvePath(dir, r), "utf8");
18732
19057
  } catch {
18733
19058
  return void 0;
18734
19059
  }
@@ -18869,7 +19194,7 @@ ${changed2.map((c) => ` - ${c}
18869
19194
  let resolvedRemotes;
18870
19195
  if (existsSync34(resolvedFile)) {
18871
19196
  try {
18872
- resolvedRemotes = JSON.parse(readFileSync30(resolvedFile, "utf8"));
19197
+ resolvedRemotes = JSON.parse(readFileSync31(resolvedFile, "utf8"));
18873
19198
  } catch {
18874
19199
  }
18875
19200
  }
@@ -18890,7 +19215,7 @@ ${changed2.map((c) => ` - ${c}
18890
19215
  isCheckout,
18891
19216
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
18892
19217
  try {
18893
- return Object.keys(parseRepoConfig(readFileSync30(resolvePath(d, f), "utf8"), f).hooks).length > 0;
19218
+ return Object.keys(parseRepoConfig(readFileSync31(resolvePath(d, f), "utf8"), f).hooks).length > 0;
18894
19219
  } catch {
18895
19220
  return false;
18896
19221
  }
@@ -18999,7 +19324,7 @@ ${changed2.map((c) => ` - ${c}
18999
19324
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
19000
19325
  `);
19001
19326
  if (existsSync34(resolvedFile)) {
19002
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
19327
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
19003
19328
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
19004
19329
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
19005
19330
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -19357,7 +19682,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19357
19682
  process.exit(2);
19358
19683
  }
19359
19684
  if (found.shipNameAdopted && !dryRun) {
19360
- writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync30(cfg.configFile, "utf8"), found.shipNameAdopted));
19685
+ writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), found.shipNameAdopted));
19361
19686
  cfg.ship.name = found.shipNameAdopted;
19362
19687
  }
19363
19688
  if (found.shipRow) {
@@ -19403,7 +19728,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19403
19728
  const hadPreviousFile = existsSync34(resolvedPath);
19404
19729
  if (hadPreviousFile) {
19405
19730
  try {
19406
- previous = JSON.parse(readFileSync30(resolvedPath, "utf8"));
19731
+ previous = JSON.parse(readFileSync31(resolvedPath, "utf8"));
19407
19732
  } catch {
19408
19733
  }
19409
19734
  }
@@ -20288,7 +20613,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20288
20613
  };
20289
20614
  const droppedRoutes = [];
20290
20615
  try {
20291
- const rawCfg = parseYaml2(readFileSync30(cfg.configFile, "utf8"));
20616
+ const rawCfg = parseYaml2(readFileSync31(cfg.configFile, "utf8"));
20292
20617
  for (const r of rawCfg?.routes ?? []) {
20293
20618
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
20294
20619
  try {
@@ -20324,7 +20649,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20324
20649
  }
20325
20650
  const host = hostPlatform();
20326
20651
  const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
20327
- const unitStatus = control?.statusCommand ? spawnSync6(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
20652
+ const unitStatus = control?.statusCommand ? spawnSync7(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
20328
20653
  const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
20329
20654
  if (!unitRunning) {
20330
20655
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
@@ -20336,7 +20661,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20336
20661
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
20337
20662
  if (flag("now") && control?.stopCommand && control.startCommand) {
20338
20663
  for (const cmd of [control.stopCommand, control.startCommand]) {
20339
- const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
20664
+ const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
20340
20665
  if (cmd === control.startCommand && (res.error || res.status !== 0)) {
20341
20666
  process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
20342
20667
  `);
@@ -20382,7 +20707,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20382
20707
  }
20383
20708
  case "log": {
20384
20709
  try {
20385
- const text = readFileSync30(cfg.ship.logFile, "utf8").split("\n");
20710
+ const text = readFileSync31(cfg.ship.logFile, "utf8").split("\n");
20386
20711
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
20387
20712
  `);
20388
20713
  } catch {
@@ -20405,7 +20730,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20405
20730
  };
20406
20731
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
20407
20732
  const tracker = new Tracker(route, cfg.ship);
20408
- const configText = readFileSync30(cfg.configFile, "utf8");
20733
+ const configText = readFileSync31(cfg.configFile, "utf8");
20409
20734
  const cfgBase = dirname20(cfg.configFile);
20410
20735
  const configured = configuredRepos(configText, route.route, cfgBase);
20411
20736
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
@@ -20443,14 +20768,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20443
20768
  if (row) {
20444
20769
  await tracker.deleteRepoRow(row.id);
20445
20770
  if (existsSync34(resolvedFile)) {
20446
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
20771
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
20447
20772
  if (raw.repoNames) delete raw.repoNames[row.id];
20448
20773
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
20449
20774
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
20450
20775
  `);
20451
20776
  }
20452
20777
  }
20453
- if (existsSync34(join37(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
20778
+ if (existsSync34(join37(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
20454
20779
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
20455
20780
  break;
20456
20781
  }
@@ -20466,7 +20791,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20466
20791
  writeFileSync23(join37(dir, "README.md"), SAMPLE_README);
20467
20792
  writeFileSync23(join37(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
20468
20793
  const g = (...a) => {
20469
- const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
20794
+ const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
20470
20795
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
20471
20796
  };
20472
20797
  g("init", "-b", "main");
@@ -20483,7 +20808,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20483
20808
  });
20484
20809
  repoId = (await tracker.createRepoRow(payload)).id;
20485
20810
  if (existsSync34(resolvedFile)) {
20486
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
20811
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
20487
20812
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
20488
20813
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
20489
20814
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -20714,7 +21039,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20714
21039
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
20715
21040
  // config-provenance display; this is the field that actually
20716
21041
  // describes where a worktree lands by default.
20717
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename11(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
21042
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
20718
21043
  worktreeParent: resolvePath(r.dir, "..")
20719
21044
  })),
20720
21045
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -20881,7 +21206,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
20881
21206
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
20882
21207
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
20883
21208
  try {
20884
- return readFileSync30(p, "utf8");
21209
+ return readFileSync31(p, "utf8");
20885
21210
  } catch {
20886
21211
  return null;
20887
21212
  }
@@ -20924,7 +21249,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
20924
21249
  );
20925
21250
  for (const r of await resolvedRepos(route)) {
20926
21251
  const { config } = r;
20927
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename11(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
21252
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
20928
21253
  process.stdout.write(
20929
21254
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
20930
21255
  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.76.0",
3
+ "version": "0.78.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",