@tablation/crew 0.76.0 → 0.77.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 (2) hide show
  1. package/dist/cli.js +339 -152
  2. package/package.json +1 -1
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 {
@@ -16467,6 +16648,12 @@ async function replayPendingWrites(ctx) {
16467
16648
  async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
16468
16649
  emit
16469
16650
  );
16651
+ await replayQueuedTicketCounters(
16652
+ ship.stateDir,
16653
+ routeSlug(route.route),
16654
+ async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
16655
+ emit
16656
+ );
16470
16657
  } catch (e) {
16471
16658
  emit.warn(`could not replay pending writes: ${e.message}`, { step: "agent" });
16472
16659
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.76.0",
3
+ "version": "0.77.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",