@tablation/crew 0.75.6 → 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.
- package/dist/cli.js +386 -155
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3997,6 +3997,9 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
3997
3997
|
function pushBranch(cwd, remote, branch, dst = branch) {
|
|
3998
3998
|
git(cwd, ["push", remote, `${branch}:${dst}`]);
|
|
3999
3999
|
}
|
|
4000
|
+
function pushBranchUpstream(cwd, remote, branch, dst = branch) {
|
|
4001
|
+
git(cwd, ["push", "--set-upstream", remote, `${branch}:${dst}`]);
|
|
4002
|
+
}
|
|
4000
4003
|
function worktrees(cwd) {
|
|
4001
4004
|
const out = gitOk(cwd, ["worktree", "list", "--porcelain"]) ?? "";
|
|
4002
4005
|
const list = [];
|
|
@@ -4850,6 +4853,158 @@ function renderEnvironment(i) {
|
|
|
4850
4853
|
return lines.join("\n");
|
|
4851
4854
|
}
|
|
4852
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
|
+
|
|
4853
5008
|
// src/agent-log.ts
|
|
4854
5009
|
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync11, rmSync as rmSync6, writeFileSync as writeFileSync7 } from "fs";
|
|
4855
5010
|
import { join as join12 } from "path";
|
|
@@ -4867,6 +5022,61 @@ var AgentLogHttpError = class extends Error {
|
|
|
4867
5022
|
this.status = status2;
|
|
4868
5023
|
}
|
|
4869
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
|
+
}
|
|
4870
5080
|
async function post(target, path, body) {
|
|
4871
5081
|
const url = `${target.baseUrl.replace(/\/+$/, "")}/api/workspaces/${target.workspaceId}${path}`;
|
|
4872
5082
|
const res = await fetch(url, {
|
|
@@ -4883,6 +5093,13 @@ async function post(target, path, body) {
|
|
|
4883
5093
|
return await res.json();
|
|
4884
5094
|
}
|
|
4885
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
|
+
}
|
|
4886
5103
|
const body = {
|
|
4887
5104
|
agentId: target.agentId,
|
|
4888
5105
|
ticketReference: run2.ticketReference,
|
|
@@ -4900,13 +5117,14 @@ async function reportAgentRun(target, run2) {
|
|
|
4900
5117
|
promptVersion: run2.promptVersion,
|
|
4901
5118
|
promptSha: run2.promptSha
|
|
4902
5119
|
};
|
|
5120
|
+
const workItem = target.tickets && ticketRecordId ? { workItemTable: target.tickets.issuesModelId, workItemRecordId: ticketRecordId } : {};
|
|
4903
5121
|
let entry;
|
|
4904
5122
|
try {
|
|
4905
|
-
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 });
|
|
4906
5124
|
} catch (e) {
|
|
4907
5125
|
if (!(e instanceof AgentLogHttpError)) throw new AgentLogUnreachableError(e.message);
|
|
4908
5126
|
if (e.status >= 500) throw new AgentLogUnreachableError(e.message);
|
|
4909
|
-
if (
|
|
5127
|
+
if (e.status !== 400 || !run2.harness && !run2.provider && !ticketRecordId) throw e;
|
|
4910
5128
|
entry = await post(target, "/agents/log", body);
|
|
4911
5129
|
}
|
|
4912
5130
|
for (const cycle of run2.cycles) {
|
|
@@ -4917,7 +5135,37 @@ async function reportAgentRun(target, run2) {
|
|
|
4917
5135
|
...cycle.role ? { role: cycle.role } : {}
|
|
4918
5136
|
});
|
|
4919
5137
|
}
|
|
4920
|
-
|
|
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
|
+
}
|
|
4921
5169
|
}
|
|
4922
5170
|
var queueDir = (stateDir) => join12(stateDir, "pending-agent-logs");
|
|
4923
5171
|
function queueAgentRun(stateDir, name, q) {
|
|
@@ -4940,7 +5188,17 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
|
|
|
4940
5188
|
try {
|
|
4941
5189
|
const apiKey = await keyFor(q.role);
|
|
4942
5190
|
if (!apiKey) continue;
|
|
4943
|
-
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
|
+
});
|
|
4944
5202
|
rmSync6(path, { force: true });
|
|
4945
5203
|
sent += 1;
|
|
4946
5204
|
log.emit(`sent a queued Agent Log report (${f})`);
|
|
@@ -4962,6 +5220,49 @@ async function replayQueuedAgentRuns(stateDir, route, keyFor, log, now = /* @__P
|
|
|
4962
5220
|
}
|
|
4963
5221
|
return sent;
|
|
4964
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
|
+
}
|
|
4965
5266
|
|
|
4966
5267
|
// src/ticket-attribution.ts
|
|
4967
5268
|
var TICKET_KEY_RE = /\b[A-Z][A-Z0-9]{1,9}-\d+\b/g;
|
|
@@ -5142,152 +5443,6 @@ import { basename as basename6 } from "path";
|
|
|
5142
5443
|
import { hostname } from "os";
|
|
5143
5444
|
import { TablationClient } from "@tablation/client";
|
|
5144
5445
|
|
|
5145
|
-
// src/contract.ts
|
|
5146
|
-
var DEFAULT_CONTRACT = {
|
|
5147
|
-
columns: {
|
|
5148
|
-
key: "issue_id",
|
|
5149
|
-
tag: "issue_tag",
|
|
5150
|
-
title: "title",
|
|
5151
|
-
status: "status",
|
|
5152
|
-
assignee: "assignee_id",
|
|
5153
|
-
priority: "priority",
|
|
5154
|
-
severity: "severity",
|
|
5155
|
-
slice: "project_id",
|
|
5156
|
-
repo: "repo_id",
|
|
5157
|
-
parent: "parent_id",
|
|
5158
|
-
blockedBy: "blocked_by",
|
|
5159
|
-
updatedAt: "updated_at",
|
|
5160
|
-
needsDesign: "needs_design",
|
|
5161
|
-
attachments: "attachments",
|
|
5162
|
-
workMode: "work_mode",
|
|
5163
|
-
reportType: "report_type",
|
|
5164
|
-
epic: "epic_id",
|
|
5165
|
-
author: "filed_by_id",
|
|
5166
|
-
ship: "ship_id",
|
|
5167
|
-
heldBy: "held_by_ship_id",
|
|
5168
|
-
resolutionNote: "resolution_note",
|
|
5169
|
-
verifiedSha: "verified_sha",
|
|
5170
|
-
prRef: "pr_ref",
|
|
5171
|
-
externalId: "external_id",
|
|
5172
|
-
position: "position"
|
|
5173
|
-
},
|
|
5174
|
-
comments: {
|
|
5175
|
-
parent: "ticket_id",
|
|
5176
|
-
body: "body",
|
|
5177
|
-
author: "team_member_id",
|
|
5178
|
-
createdAt: "created_at",
|
|
5179
|
-
ship: "ship_id",
|
|
5180
|
-
publicReply: "is_public_reply",
|
|
5181
|
-
authorName: "author_name"
|
|
5182
|
-
},
|
|
5183
|
-
statuses: {
|
|
5184
|
-
open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
|
|
5185
|
-
resolved: [
|
|
5186
|
-
"verified",
|
|
5187
|
-
"closed_deployed",
|
|
5188
|
-
"closed_wont_fix",
|
|
5189
|
-
"closed_duplicate",
|
|
5190
|
-
"closed_completed",
|
|
5191
|
-
"closed_cant_reproduce"
|
|
5192
|
-
],
|
|
5193
|
-
approved: "accepted",
|
|
5194
|
-
parked: "blocked",
|
|
5195
|
-
building: "in_progress",
|
|
5196
|
-
handoff: "fixed",
|
|
5197
|
-
verifying: "qa",
|
|
5198
|
-
verified: "verified",
|
|
5199
|
-
deployed: "closed_deployed",
|
|
5200
|
-
needsHuman: "needs_info",
|
|
5201
|
-
reviewing: null,
|
|
5202
|
-
wontFix: "closed_wont_fix",
|
|
5203
|
-
draft: "draft"
|
|
5204
|
-
},
|
|
5205
|
-
epics: {
|
|
5206
|
-
statusColumn: "status",
|
|
5207
|
-
keyColumn: "epic_id",
|
|
5208
|
-
statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
|
|
5209
|
-
},
|
|
5210
|
-
priorityOrder: ["p0", "p1", "p2", "p3"],
|
|
5211
|
-
severityOrder: ["s1", "s2", "s3", "s4"],
|
|
5212
|
-
unknownPriorityRank: 2,
|
|
5213
|
-
coordinatingValue: "coordinating",
|
|
5214
|
-
manualWorkMode: "manual",
|
|
5215
|
-
hybridWorkMode: "hybrid",
|
|
5216
|
-
repoExemptReportTypes: ["coordinating", "question", "investigation"]
|
|
5217
|
-
};
|
|
5218
|
-
function closedStatuses(c) {
|
|
5219
|
-
return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
|
|
5220
|
-
}
|
|
5221
|
-
function resolveContract(override) {
|
|
5222
|
-
if (!override) return DEFAULT_CONTRACT;
|
|
5223
|
-
return {
|
|
5224
|
-
columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
|
|
5225
|
-
comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
|
|
5226
|
-
statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
|
|
5227
|
-
epics: {
|
|
5228
|
-
...DEFAULT_CONTRACT.epics,
|
|
5229
|
-
...override.epics,
|
|
5230
|
-
statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
|
|
5231
|
-
},
|
|
5232
|
-
priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
|
|
5233
|
-
severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
|
|
5234
|
-
unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
|
|
5235
|
-
coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
|
|
5236
|
-
manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
|
|
5237
|
-
hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
|
|
5238
|
-
// A workspace overriding `coordinatingValue` alone (renaming the rollup
|
|
5239
|
-
// marker) still gets that renamed value exempted here by default,
|
|
5240
|
-
// without also having to repeat it — only an explicit
|
|
5241
|
-
// `repoExemptReportTypes` overrides this derived default.
|
|
5242
|
-
repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
|
|
5243
|
-
};
|
|
5244
|
-
}
|
|
5245
|
-
function validateContract(c) {
|
|
5246
|
-
const problems = [];
|
|
5247
|
-
const open = new Set(c.statuses.open);
|
|
5248
|
-
const resolved = new Set(c.statuses.resolved);
|
|
5249
|
-
for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
|
|
5250
|
-
const v = c.statuses[role];
|
|
5251
|
-
if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
|
|
5252
|
-
}
|
|
5253
|
-
if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
|
|
5254
|
-
problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
|
|
5255
|
-
}
|
|
5256
|
-
if (!resolved.has(c.statuses.deployed)) {
|
|
5257
|
-
problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
|
|
5258
|
-
}
|
|
5259
|
-
if (resolved.has(c.statuses.handoff)) {
|
|
5260
|
-
problems.push(
|
|
5261
|
-
`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`
|
|
5262
|
-
);
|
|
5263
|
-
}
|
|
5264
|
-
if (!resolved.has(c.statuses.wontFix)) {
|
|
5265
|
-
problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
|
|
5266
|
-
}
|
|
5267
|
-
if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
|
|
5268
|
-
problems.push(
|
|
5269
|
-
`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`
|
|
5270
|
-
);
|
|
5271
|
-
}
|
|
5272
|
-
if (resolved.has(c.statuses.approved)) {
|
|
5273
|
-
problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
|
|
5274
|
-
}
|
|
5275
|
-
if (c.statuses.parked === c.statuses.approved) {
|
|
5276
|
-
problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
|
|
5277
|
-
}
|
|
5278
|
-
const epicValues = Object.values(c.epics.statuses);
|
|
5279
|
-
if (new Set(epicValues).size !== epicValues.length) {
|
|
5280
|
-
problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
|
|
5281
|
-
}
|
|
5282
|
-
if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
|
|
5283
|
-
if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
|
|
5284
|
-
for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
|
|
5285
|
-
if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
|
|
5286
|
-
}
|
|
5287
|
-
return problems;
|
|
5288
|
-
}
|
|
5289
|
-
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.";
|
|
5290
|
-
|
|
5291
5446
|
// src/state.ts
|
|
5292
5447
|
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7, rmSync as rmSync7, statSync as statSync4 } from "fs";
|
|
5293
5448
|
import { join as join13 } from "path";
|
|
@@ -6989,7 +7144,15 @@ function inspectHandoff(dir, cfg, ticket, role) {
|
|
|
6989
7144
|
}
|
|
6990
7145
|
return { kind: "pushed", branch, sha: origin, remote };
|
|
6991
7146
|
}
|
|
6992
|
-
function
|
|
7147
|
+
function pushableBounce(dir, v) {
|
|
7148
|
+
if (!v.branch || !v.local) return false;
|
|
7149
|
+
return !v.origin || isAncestor(dir, v.origin, v.local);
|
|
7150
|
+
}
|
|
7151
|
+
function describeHandoffPush(dir, cfg, ticket, role) {
|
|
7152
|
+
const v = inspectHandoff(dir, cfg, ticket, role);
|
|
7153
|
+
return v.kind === "bounce" && pushableBounce(dir, v) ? `would push ${v.branch} to ${v.remote}` : null;
|
|
7154
|
+
}
|
|
7155
|
+
function bounceComment(v, shipName2, pushError) {
|
|
6993
7156
|
return `${HANDOFF_BOUNCED_MARKER}
|
|
6994
7157
|
|
|
6995
7158
|
**Hand-off bounced: the branch is not on \`${v.remote}\` at the tip this ticket was built to.**
|
|
@@ -6998,7 +7161,7 @@ ${shipName2 ? `Ship: ${shipName2}
|
|
|
6998
7161
|
` : ""}Local HEAD: ${v.local ? `\`${v.local}\`` : "(no local branch)"}
|
|
6999
7162
|
\`${v.remote}\`: ${v.origin ? `\`${v.origin}\`` : v.branch ? "(branch missing)" : "(no branch to look for)"}
|
|
7000
7163
|
|
|
7001
|
-
${v.reason[0].toUpperCase()}${v.reason.slice(1)}. Set back to \`in_progress\`, still held by this ship; push with a plain \`git push --set-upstream ${v.remote} <branch>\` (never \`--force\`) and set \`fixed\` again. Your brief authorizes this push: it is the hand-off, not a release, so do it now rather than asking for permission.`;
|
|
7164
|
+
${v.reason[0].toUpperCase()}${v.reason.slice(1)}. ${pushError ? ` The runner tried a plain push and git refused it: \`${pushError}\`.` : ""} Set back to \`in_progress\`, still held by this ship; push with a plain \`git push --set-upstream ${v.remote} <branch>\` (never \`--force\`) and set \`fixed\` again. Your brief authorizes this push: it is the hand-off, not a release, so do it now rather than asking for permission.`;
|
|
7002
7165
|
}
|
|
7003
7166
|
async function takenOverSince(i, ticketRow) {
|
|
7004
7167
|
if (!i.since || !i.writer.events) return false;
|
|
@@ -7048,12 +7211,35 @@ async function enforceHandoff(i) {
|
|
|
7048
7211
|
i.log.warn(`${t.issue_id} has no local branch on this ship, so it is not this ship's hand-off to bounce: ${v.reason}`, log);
|
|
7049
7212
|
return { kind: "unverified", why: "no local branch on this ship" };
|
|
7050
7213
|
}
|
|
7214
|
+
let pushError;
|
|
7215
|
+
if (pushableBounce(i.dir, v)) {
|
|
7216
|
+
const wt = worktrees(i.dir).find((w) => w.branch === v.branch);
|
|
7217
|
+
try {
|
|
7218
|
+
pushBranchUpstream(wt?.path ?? i.dir, v.remote, v.branch);
|
|
7219
|
+
const again = inspectHandoff(i.dir, i.cfg, t, i.role);
|
|
7220
|
+
if (again.kind === "pushed") {
|
|
7221
|
+
await i.writer.postEvent(t.id, pushedEventBody(again.sha, again.remote, again.branch), i.seatId, "pushed");
|
|
7222
|
+
await i.writer.postEvent(
|
|
7223
|
+
t.id,
|
|
7224
|
+
`The runner pushed \`${again.branch}\` to \`${again.remote}\` on ${i.shipName ? `${i.shipName}'s` : "this ship's"} behalf: the ${i.role} seat set \`${i.contract.statuses.handoff}\` without pushing. Plain fast-forward push.${i.cfg.hooks.ci_status ? " The repo's `ci_status` hook was NOT run by the hand-off guard, so CI was not checked by the builder." : ""}`,
|
|
7225
|
+
i.seatId,
|
|
7226
|
+
"pushed_by_runner"
|
|
7227
|
+
).catch(() => i.log.warn("pushed, but the note failed to post", log));
|
|
7228
|
+
i.log.emit(`${t.issue_id}: pushed ${again.branch} to ${again.remote} on the seat's behalf at ${again.sha.slice(0, 7)} \u2014 handed on to QA`, log);
|
|
7229
|
+
return { kind: "pushed", sha: again.sha };
|
|
7230
|
+
}
|
|
7231
|
+
pushError = `the push succeeded but ${again.kind === "bounce" ? again.reason : again.why}`;
|
|
7232
|
+
} catch (e) {
|
|
7233
|
+
pushError = e.message;
|
|
7234
|
+
}
|
|
7235
|
+
i.log.warn(`${t.issue_id}: the runner's own push failed: ${pushError}`, log);
|
|
7236
|
+
}
|
|
7051
7237
|
await i.writer.updateTicket(t.id, {
|
|
7052
7238
|
status: i.contract.statuses.building,
|
|
7053
7239
|
...t.assignee_id ? {} : { assignee_id: i.seatId },
|
|
7054
7240
|
...i.hold && !heldBy ? { [i.hold.column]: i.hold.shipId } : {}
|
|
7055
7241
|
});
|
|
7056
|
-
await i.writer.postEvent(t.id, bounceComment(v, i.shipName), i.seatId, "bounced").catch(() => {
|
|
7242
|
+
await i.writer.postEvent(t.id, bounceComment(v, i.shipName, pushError), i.seatId, "bounced").catch(() => {
|
|
7057
7243
|
i.log.warn("bounced, but the note failed to post", log);
|
|
7058
7244
|
});
|
|
7059
7245
|
i.log.warn(`${t.issue_id} bounced back to ${i.contract.statuses.building}: ${v.reason}`, log);
|
|
@@ -8110,12 +8296,26 @@ function planAgentRun(o) {
|
|
|
8110
8296
|
// resolved yet has nothing to authenticate the MCP connection with either.
|
|
8111
8297
|
mcp: o.apiKey ? { url: `${o.route.baseUrl}/api/mcp`, keyVar: API_KEY_VAR } : void 0
|
|
8112
8298
|
});
|
|
8299
|
+
const columns = resolveContract(o.route.contract).columns;
|
|
8113
8300
|
const agentLog = o.route.resolved && o.apiKey ? {
|
|
8114
8301
|
baseUrl: o.route.baseUrl,
|
|
8115
8302
|
workspaceId: o.route.resolved.workspaceId,
|
|
8116
8303
|
apiKey: o.apiKey,
|
|
8117
8304
|
userAgent: o.ship.userAgent,
|
|
8118
|
-
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
|
+
} } : {}
|
|
8119
8319
|
} : void 0;
|
|
8120
8320
|
return {
|
|
8121
8321
|
role: o.role,
|
|
@@ -8485,7 +8685,22 @@ async function finishRun(id, obs, emit, startedAt, end, agentLogKey) {
|
|
|
8485
8685
|
promptSha: id.promptSha
|
|
8486
8686
|
};
|
|
8487
8687
|
try {
|
|
8488
|
-
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
|
+
}
|
|
8489
8704
|
} catch (e) {
|
|
8490
8705
|
if (e instanceof AgentLogUnreachableError) {
|
|
8491
8706
|
try {
|
|
@@ -16433,6 +16648,12 @@ async function replayPendingWrites(ctx) {
|
|
|
16433
16648
|
async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
|
|
16434
16649
|
emit
|
|
16435
16650
|
);
|
|
16651
|
+
await replayQueuedTicketCounters(
|
|
16652
|
+
ship.stateDir,
|
|
16653
|
+
routeSlug(route.route),
|
|
16654
|
+
async (role) => (await resolveSeatKey(route, role, ctx.getSessionStore)).apiKey,
|
|
16655
|
+
emit
|
|
16656
|
+
);
|
|
16436
16657
|
} catch (e) {
|
|
16437
16658
|
emit.warn(`could not replay pending writes: ${e.message}`, { step: "agent" });
|
|
16438
16659
|
}
|
|
@@ -18250,6 +18471,16 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18250
18471
|
if (dryRun) {
|
|
18251
18472
|
process.stdout.write(`${describePlan(plan)}
|
|
18252
18473
|
`);
|
|
18474
|
+
if (workingId && (current === "dev" || current === "design")) {
|
|
18475
|
+
try {
|
|
18476
|
+
const t = await tracker2.ticket(workingId);
|
|
18477
|
+
const r = repoConfigFor(route, t);
|
|
18478
|
+
const would = r && describeHandoffPush(r.target.dir, r.cfg, t, current);
|
|
18479
|
+
if (would) process.stdout.write(`${would}
|
|
18480
|
+
`);
|
|
18481
|
+
} catch {
|
|
18482
|
+
}
|
|
18483
|
+
}
|
|
18253
18484
|
break;
|
|
18254
18485
|
}
|
|
18255
18486
|
const before = snapshot(await tracker2.openTickets());
|
package/package.json
CHANGED