@gh-symphony/cli 0.0.20 → 0.0.22
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/README.md +66 -2
- package/dist/chunk-2TSM3INR.js +1085 -0
- package/dist/chunk-2UW7NQLX.js +684 -0
- package/dist/{chunk-MVRF7BES.js → chunk-36KYEDEO.js} +10 -1
- package/dist/{chunk-TILHWBP6.js → chunk-C67H3OUL.js} +239 -36
- package/dist/{chunk-C7G7RJ4G.js → chunk-DDL4BWSL.js} +1 -1
- package/dist/{chunk-XN5ABWZ6.js → chunk-DFLXHNYQ.js} +26 -30
- package/dist/{chunk-EKKT5USP.js → chunk-E7HYEEZD.js} +487 -133
- package/dist/chunk-EEQQWTXS.js +3257 -0
- package/dist/chunk-GDE6FYN4.js +26 -0
- package/dist/{chunk-Y6TYJMNT.js → chunk-GSX2FV3M.js} +10 -16
- package/dist/{chunk-RN2PACNV.js → chunk-HMLBBZNY.js} +731 -75
- package/dist/{chunk-5NV3LSAJ.js → chunk-IWFX2FMA.js} +5 -1
- package/dist/{chunk-HZVDTAPS.js → chunk-PUDXVBSN.js} +1549 -1458
- package/dist/{chunk-ROGRTUFI.js → chunk-QIRE2VXS.js} +14 -3
- package/dist/{chunk-3AWF54PI.js → chunk-ZHOKYUO3.js} +394 -42
- package/dist/{config-cmd-DNXNL26Z.js → config-cmd-Z3A7V6NC.js} +1 -1
- package/dist/{doctor-IYHCFXOZ.js → doctor-EJUMPBMW.js} +105 -40
- package/dist/index.js +112 -24
- package/dist/{init-KZT6YNOH.js → init-54HMKNYI.js} +8 -3
- package/dist/{logs-6JKKYDGJ.js → logs-GTZ4U5JE.js} +2 -2
- package/dist/project-RMYMZSFV.js +25 -0
- package/dist/{recover-5KQI7WH5.js → recover-LTLKMTRX.js} +7 -5
- package/dist/repo-WI7GF6XQ.js +749 -0
- package/dist/{run-ETC5UTRA.js → run-IHN3ZL35.js} +21 -7
- package/dist/{setup-VWB7RZUQ.js → setup-TZJSM3QV.js} +53 -14
- package/dist/start-RTAHQMR2.js +19 -0
- package/dist/status-F4D52OVK.js +12 -0
- package/dist/stop-MDKMJPVR.js +10 -0
- package/dist/{upgrade-3YNF3VKY.js → upgrade-O33S2SJK.js} +2 -2
- package/dist/{version-NUBTTOG7.js → version-CW54Q7BK.js} +1 -1
- package/dist/worker-entry.js +848 -693
- package/dist/{workflow-TBIFY5MO.js → workflow-L3KT6HB7.js} +177 -11
- package/package.json +4 -2
- package/dist/chunk-M3IFVLQS.js +0 -1155
- package/dist/project-UUVHS3ZR.js +0 -22
- package/dist/repo-HDDE7OUI.js +0 -321
- package/dist/start-ENFLZUI6.js +0 -16
- package/dist/status-QSCFVGRQ.js +0 -11
- package/dist/stop-7MFCBQVW.js +0 -9
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveTrackerAdapter
|
|
4
|
+
} from "./chunk-2TSM3INR.js";
|
|
2
5
|
import {
|
|
3
6
|
DEFAULT_MAX_FAILURE_RETRIES,
|
|
4
7
|
DEFAULT_WORKFLOW_LIFECYCLE,
|
|
@@ -24,22 +27,476 @@ import {
|
|
|
24
27
|
readJsonFile,
|
|
25
28
|
renderPrompt,
|
|
26
29
|
resolveIssueWorkspaceDirectory,
|
|
30
|
+
resolveWorkflowRuntimeCommand,
|
|
31
|
+
resolveWorkflowRuntimeTimeouts,
|
|
27
32
|
safeReadDir,
|
|
28
33
|
scheduleRetryAt
|
|
29
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-EEQQWTXS.js";
|
|
35
|
+
|
|
36
|
+
// ../tracker-file/src/file-tracker-adapter.ts
|
|
37
|
+
import { readFile } from "fs/promises";
|
|
38
|
+
function requireTrackerSetting(project, key) {
|
|
39
|
+
const value = project.tracker.settings?.[key];
|
|
40
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Tracker adapter "file" requires the "${key}" setting.`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function parseIssueNumber(identifier) {
|
|
48
|
+
const match = identifier.match(/#(\d+)$/);
|
|
49
|
+
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
50
|
+
}
|
|
51
|
+
function isValidIssueShape(entry) {
|
|
52
|
+
if (!entry || typeof entry !== "object") return false;
|
|
53
|
+
const e = entry;
|
|
54
|
+
return typeof e.id === "string" && typeof e.identifier === "string" && typeof e.state === "string" && e.repository !== null && typeof e.repository === "object" && e.tracker !== null && typeof e.tracker === "object";
|
|
55
|
+
}
|
|
56
|
+
var fileTrackerAdapter = {
|
|
57
|
+
async listIssues(project) {
|
|
58
|
+
const issuesPath = requireTrackerSetting(project, "issuesPath");
|
|
59
|
+
try {
|
|
60
|
+
const raw = await readFile(issuesPath, "utf-8");
|
|
61
|
+
const parsed = JSON.parse(raw);
|
|
62
|
+
if (!Array.isArray(parsed)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Expected an array of issues in ${issuesPath}, got ${typeof parsed}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
const valid = [];
|
|
68
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
69
|
+
if (isValidIssueShape(parsed[i])) {
|
|
70
|
+
valid.push(parsed[i]);
|
|
71
|
+
} else {
|
|
72
|
+
process.stderr.write(
|
|
73
|
+
`[tracker-file] Skipping invalid issue at index ${i} in ${issuesPath}
|
|
74
|
+
`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return valid;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
if (err instanceof SyntaxError) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
async listIssuesByStates(project, states) {
|
|
90
|
+
if (states.length === 0) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const issues = await this.listIssues(project);
|
|
94
|
+
const normalizedStates = new Set(
|
|
95
|
+
states.map((state) => state.trim().toLowerCase())
|
|
96
|
+
);
|
|
97
|
+
return issues.filter(
|
|
98
|
+
(issue) => normalizedStates.has(issue.state.trim().toLowerCase())
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
async fetchIssueStatesByIds(project, issueIds) {
|
|
102
|
+
if (issueIds.length === 0) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const issues = await this.listIssues(project);
|
|
106
|
+
const ids = new Set(issueIds);
|
|
107
|
+
return issues.filter((issue) => ids.has(issue.id));
|
|
108
|
+
},
|
|
109
|
+
buildWorkerEnvironment(_project, _issue) {
|
|
110
|
+
return {
|
|
111
|
+
SYMPHONY_FILE_TRACKER: "true"
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
reviveIssue(project, run) {
|
|
115
|
+
return {
|
|
116
|
+
id: run.issueId,
|
|
117
|
+
identifier: run.issueIdentifier,
|
|
118
|
+
number: parseIssueNumber(run.issueIdentifier),
|
|
119
|
+
title: run.issueTitle ?? run.issueIdentifier,
|
|
120
|
+
description: null,
|
|
121
|
+
priority: null,
|
|
122
|
+
state: run.issueState,
|
|
123
|
+
branchName: null,
|
|
124
|
+
url: null,
|
|
125
|
+
labels: [],
|
|
126
|
+
blockedBy: [],
|
|
127
|
+
createdAt: null,
|
|
128
|
+
updatedAt: null,
|
|
129
|
+
repository: run.repository,
|
|
130
|
+
tracker: {
|
|
131
|
+
adapter: "file",
|
|
132
|
+
bindingId: project.tracker.bindingId,
|
|
133
|
+
itemId: run.issueId
|
|
134
|
+
},
|
|
135
|
+
metadata: {}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// ../orchestrator/src/tracker-adapters.ts
|
|
141
|
+
var localAdapters = /* @__PURE__ */ new Map([
|
|
142
|
+
["file", fileTrackerAdapter]
|
|
143
|
+
]);
|
|
144
|
+
function resolveTrackerAdapter2(tracker) {
|
|
145
|
+
const local = localAdapters.get(tracker.adapter);
|
|
146
|
+
if (local) return local;
|
|
147
|
+
return resolveTrackerAdapter(tracker);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ../orchestrator/src/explain.ts
|
|
151
|
+
var MAX_FAILURE_RETRIES_EXCEEDED_REASON = "max_failure_retries_exceeded";
|
|
152
|
+
function explainIssueDispatch(input) {
|
|
153
|
+
const parsed = parseIssueIdentifier(input.identifier);
|
|
154
|
+
const repository = parsed ? `${parsed.owner}/${parsed.name}` : input.issue ? `${input.issue.repository.owner}/${input.issue.repository.name}` : "unknown";
|
|
155
|
+
const issue = input.issue;
|
|
156
|
+
const checks = [];
|
|
157
|
+
checks.push(explainRepositoryLinked(input.projectRepository, repository));
|
|
158
|
+
checks.push(explainProjectItemPresent(input.identifier, issue));
|
|
159
|
+
if (!issue) {
|
|
160
|
+
const dispatchable2 = false;
|
|
161
|
+
const blocking2 = checks.filter((check) => check.status === "block");
|
|
162
|
+
const summary2 = blocking2.length > 0 ? `Not dispatchable: ${blocking2[0].message}` : "Not dispatchable: the issue is not present in the managed GitHub Project item set.";
|
|
163
|
+
return {
|
|
164
|
+
issue: {
|
|
165
|
+
identifier: input.identifier,
|
|
166
|
+
id: null,
|
|
167
|
+
state: null,
|
|
168
|
+
repository,
|
|
169
|
+
title: null,
|
|
170
|
+
url: null
|
|
171
|
+
},
|
|
172
|
+
dispatchable: dispatchable2,
|
|
173
|
+
summary: summary2,
|
|
174
|
+
checks
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
checks.push(explainWorkflowState(issue, input.lifecycle));
|
|
178
|
+
checks.push(explainBlockers(issue, input.lifecycle, input.allIssues));
|
|
179
|
+
checks.push(explainRuntimeOwnership(issue, input.issueRecords, input.runs));
|
|
180
|
+
checks.push(
|
|
181
|
+
explainDispatchLimits(
|
|
182
|
+
issue,
|
|
183
|
+
input.runs,
|
|
184
|
+
input.activeRunCount,
|
|
185
|
+
input.maxConcurrentAgents,
|
|
186
|
+
input.maxConcurrentAgentsByState
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
const blocking = checks.filter((check) => check.status === "block");
|
|
190
|
+
const dispatchable = blocking.length === 0;
|
|
191
|
+
const summary = dispatchable ? "Dispatchable: no blocking project, workflow, runtime, or budget condition was found." : `Not dispatchable: ${blocking[0].message}`;
|
|
192
|
+
return {
|
|
193
|
+
issue: {
|
|
194
|
+
identifier: issue.identifier,
|
|
195
|
+
id: issue.id,
|
|
196
|
+
state: issue.state,
|
|
197
|
+
repository: `${issue.repository.owner}/${issue.repository.name}`,
|
|
198
|
+
title: issue.title,
|
|
199
|
+
url: issue.url
|
|
200
|
+
},
|
|
201
|
+
dispatchable,
|
|
202
|
+
summary,
|
|
203
|
+
checks
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function isIssueCandidateEligibleWithReason(issue, lifecycle, issues) {
|
|
207
|
+
if (!isStateActive(issue.state, lifecycle)) {
|
|
208
|
+
return { eligible: false, reason: "inactive_state" };
|
|
209
|
+
}
|
|
210
|
+
if (!issueHasBlockingDependency(issue, lifecycle, issues)) {
|
|
211
|
+
return { eligible: true, reason: null };
|
|
212
|
+
}
|
|
213
|
+
return { eligible: false, reason: "blocked" };
|
|
214
|
+
}
|
|
215
|
+
function hasConvergenceLockedRunForIssue(runs, issueId, issueState, issueUpdatedAt) {
|
|
216
|
+
const latestRun = latestRunForIssue(runs, issueId);
|
|
217
|
+
if (latestRun?.runtimeSession?.exitClassification !== "convergence-detected" || latestRun.issueState !== issueState) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const convergedAtMs = parseTimestampMs(
|
|
221
|
+
latestRun.completedAt ?? latestRun.updatedAt
|
|
222
|
+
);
|
|
223
|
+
const issueUpdatedAtMs = parseTimestampMs(issueUpdatedAt);
|
|
224
|
+
if (convergedAtMs === null || issueUpdatedAtMs === null) {
|
|
225
|
+
return latestRun;
|
|
226
|
+
}
|
|
227
|
+
return issueUpdatedAtMs <= convergedAtMs ? latestRun : null;
|
|
228
|
+
}
|
|
229
|
+
function isIssueOrchestrationClaimedState(state) {
|
|
230
|
+
return state === "claimed" || state === "running" || state === "retry_queued";
|
|
231
|
+
}
|
|
232
|
+
function isActiveRunRecordStatus(status) {
|
|
233
|
+
return status === "pending" || status === "starting" || status === "running" || status === "retrying";
|
|
234
|
+
}
|
|
235
|
+
function explainRepositoryLinked(projectRepository, repository) {
|
|
236
|
+
if (!projectRepository) {
|
|
237
|
+
return {
|
|
238
|
+
id: "repository_linked",
|
|
239
|
+
status: "warn",
|
|
240
|
+
message: "No repository is configured for the active managed project.",
|
|
241
|
+
hint: "Run 'gh-symphony repo add <owner/name>' or re-run 'gh-symphony project add'."
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
const configured = `${projectRepository.owner}/${projectRepository.name}`;
|
|
245
|
+
const linked = normalizeIdentifier(configured) === normalizeIdentifier(repository);
|
|
246
|
+
return {
|
|
247
|
+
id: "repository_linked",
|
|
248
|
+
status: linked ? "pass" : "block",
|
|
249
|
+
message: linked ? `Repository ${repository} is linked to the active managed project.` : `Repository ${repository} is not the active managed project repository (${configured}).`,
|
|
250
|
+
details: { configuredRepository: configured, issueRepository: repository },
|
|
251
|
+
hint: linked ? void 0 : "Run 'gh-symphony repo add <owner/name>' or select the correct project with 'gh-symphony project switch'."
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function explainProjectItemPresent(identifier, issue) {
|
|
255
|
+
return {
|
|
256
|
+
id: "project_item_present",
|
|
257
|
+
status: issue ? "pass" : "block",
|
|
258
|
+
message: issue ? "Issue is present in the bound GitHub Project item set." : `Issue ${identifier} was not returned by the bound GitHub Project item set.`,
|
|
259
|
+
details: issue ? { itemId: issue.tracker.itemId } : void 0,
|
|
260
|
+
hint: issue ? void 0 : "Add the issue to the GitHub Project or run 'gh-symphony project status' to confirm the active project."
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function explainWorkflowState(issue, lifecycle) {
|
|
264
|
+
if (isStateActive(issue.state, lifecycle)) {
|
|
265
|
+
return {
|
|
266
|
+
id: "workflow_state",
|
|
267
|
+
status: "pass",
|
|
268
|
+
message: `Project state "${issue.state}" maps to an active state in WORKFLOW.md.`,
|
|
269
|
+
details: { activeStates: lifecycle.activeStates }
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
const role = isStateTerminal(issue.state, lifecycle) ? "terminal" : "wait";
|
|
273
|
+
return {
|
|
274
|
+
id: "workflow_state",
|
|
275
|
+
status: "block",
|
|
276
|
+
message: `Project state "${issue.state}" maps to ${role}, not active, in WORKFLOW.md.`,
|
|
277
|
+
details: {
|
|
278
|
+
activeStates: lifecycle.activeStates,
|
|
279
|
+
terminalStates: lifecycle.terminalStates,
|
|
280
|
+
blockerCheckStates: lifecycle.blockerCheckStates
|
|
281
|
+
},
|
|
282
|
+
hint: "Move the GitHub Project item to an active state or run 'gh-symphony workflow preview' to inspect WORKFLOW.md state mappings."
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function explainBlockers(issue, lifecycle, issues) {
|
|
286
|
+
if (!matchesWorkflowState(issue.state, lifecycle.blockerCheckStates)) {
|
|
287
|
+
return {
|
|
288
|
+
id: "blockers",
|
|
289
|
+
status: "pass",
|
|
290
|
+
message: `Blocker checks do not apply to state "${issue.state}".`,
|
|
291
|
+
details: { blockerCheckStates: lifecycle.blockerCheckStates }
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
const blockers = unresolvedBlockers(issue, lifecycle, issues);
|
|
295
|
+
if (blockers.length === 0) {
|
|
296
|
+
return {
|
|
297
|
+
id: "blockers",
|
|
298
|
+
status: "pass",
|
|
299
|
+
message: "No unresolved blockers prevent dispatch.",
|
|
300
|
+
details: { blockedBy: issue.blockedBy }
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
id: "blockers",
|
|
305
|
+
status: "block",
|
|
306
|
+
message: `Issue has ${blockers.length} unresolved blocker${blockers.length === 1 ? "" : "s"}.`,
|
|
307
|
+
details: { blockers },
|
|
308
|
+
hint: "Move blocker issues to a terminal state or update the blocker relationship in GitHub."
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function explainRuntimeOwnership(issue, issueRecords, runs) {
|
|
312
|
+
const record = issueRecords.find(
|
|
313
|
+
(candidate) => candidate.issueId === issue.id || candidate.identifier === issue.identifier
|
|
314
|
+
);
|
|
315
|
+
const latestRun = latestRunForIssue(runs, issue.id);
|
|
316
|
+
const activeRun = runs.find(
|
|
317
|
+
(run) => run.issueId === issue.id && isActiveRunRecordStatus(run.status)
|
|
318
|
+
);
|
|
319
|
+
if (activeRun) {
|
|
320
|
+
return {
|
|
321
|
+
id: "runtime_ownership",
|
|
322
|
+
status: "block",
|
|
323
|
+
message: `Existing ${activeRun.status} run ${activeRun.runId} already owns the issue.`,
|
|
324
|
+
details: {
|
|
325
|
+
runId: activeRun.runId,
|
|
326
|
+
status: activeRun.status,
|
|
327
|
+
retryKind: activeRun.retryKind,
|
|
328
|
+
nextRetryAt: activeRun.nextRetryAt
|
|
329
|
+
},
|
|
330
|
+
hint: "Run 'gh-symphony status' or 'gh-symphony logs --issue <owner/repo#number>' to inspect the current owner."
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (record && isIssueOrchestrationClaimedState(record.state)) {
|
|
334
|
+
return {
|
|
335
|
+
id: "runtime_ownership",
|
|
336
|
+
status: "block",
|
|
337
|
+
message: `Issue is already claimed by orchestration state "${record.state}".`,
|
|
338
|
+
details: {
|
|
339
|
+
state: record.state,
|
|
340
|
+
currentRunId: record.currentRunId,
|
|
341
|
+
retryEntry: record.retryEntry
|
|
342
|
+
},
|
|
343
|
+
hint: "Run 'gh-symphony status' to inspect active and retrying work."
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
const convergenceRun = hasConvergenceLockedRunForIssue(
|
|
347
|
+
runs,
|
|
348
|
+
issue.id,
|
|
349
|
+
issue.state,
|
|
350
|
+
issue.updatedAt
|
|
351
|
+
);
|
|
352
|
+
if (convergenceRun) {
|
|
353
|
+
return {
|
|
354
|
+
id: "runtime_ownership",
|
|
355
|
+
status: "block",
|
|
356
|
+
message: `Latest run ${convergenceRun.runId} is convergence-locked for state "${issue.state}".`,
|
|
357
|
+
details: {
|
|
358
|
+
runId: convergenceRun.runId,
|
|
359
|
+
completedAt: convergenceRun.completedAt,
|
|
360
|
+
lastError: convergenceRun.lastError
|
|
361
|
+
},
|
|
362
|
+
hint: "Update the GitHub Project item or issue activity to trigger a newer tracker timestamp after resolving the unchanged workspace diff."
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (record && record.failureRetryCount > 0 && latestRun?.status === "suppressed" && latestRun.issueState === issue.state && latestRun.lastError?.includes(MAX_FAILURE_RETRIES_EXCEEDED_REASON)) {
|
|
366
|
+
const issueUpdatedAtMs = parseTimestampMs(issue.updatedAt);
|
|
367
|
+
const suppressedAtMs = parseTimestampMs(
|
|
368
|
+
latestRun.completedAt ?? latestRun.updatedAt
|
|
369
|
+
);
|
|
370
|
+
if (issueUpdatedAtMs === null || suppressedAtMs === null || issueUpdatedAtMs <= suppressedAtMs) {
|
|
371
|
+
return {
|
|
372
|
+
id: "runtime_ownership",
|
|
373
|
+
status: "block",
|
|
374
|
+
message: "Failure retry limit has suppressed redispatch for the current tracker state.",
|
|
375
|
+
details: {
|
|
376
|
+
failureRetryCount: record.failureRetryCount,
|
|
377
|
+
runId: latestRun.runId,
|
|
378
|
+
lastError: latestRun.lastError
|
|
379
|
+
},
|
|
380
|
+
hint: "Fix the underlying failure and update the GitHub Project item or issue to create a newer tracker timestamp."
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
id: "runtime_ownership",
|
|
386
|
+
status: "pass",
|
|
387
|
+
message: "No active run, retry, convergence lock, or suppression owns the issue.",
|
|
388
|
+
details: record ? {
|
|
389
|
+
orchestrationState: record.state,
|
|
390
|
+
currentRunId: record.currentRunId,
|
|
391
|
+
latestRunId: latestRun?.runId ?? null
|
|
392
|
+
} : void 0
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function explainDispatchLimits(issue, runs, activeRunCount, maxConcurrentAgents, maxConcurrentAgentsByState) {
|
|
396
|
+
if (activeRunCount >= maxConcurrentAgents) {
|
|
397
|
+
return {
|
|
398
|
+
id: "dispatch_limits",
|
|
399
|
+
status: "block",
|
|
400
|
+
message: `Project concurrency is full (${activeRunCount}/${maxConcurrentAgents}).`,
|
|
401
|
+
details: { activeRunCount, maxConcurrentAgents },
|
|
402
|
+
hint: "Wait for an active run to finish or adjust agent.max_concurrent_agents in WORKFLOW.md."
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const stateLimit = maxConcurrentAgentsByState[issue.state];
|
|
406
|
+
if (stateLimit !== void 0) {
|
|
407
|
+
const activeInState = runs.filter(
|
|
408
|
+
(run) => run.issueState === issue.state && isActiveRunRecordStatus(run.status)
|
|
409
|
+
).length;
|
|
410
|
+
if (activeInState >= stateLimit) {
|
|
411
|
+
return {
|
|
412
|
+
id: "dispatch_limits",
|
|
413
|
+
status: "block",
|
|
414
|
+
message: `State concurrency is full for "${issue.state}" (${activeInState}/${stateLimit}).`,
|
|
415
|
+
details: { activeInState, stateLimit, state: issue.state },
|
|
416
|
+
hint: "Wait for a same-state run to finish or adjust agent.max_concurrent_agents_by_state in WORKFLOW.md."
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
id: "dispatch_limits",
|
|
422
|
+
status: "pass",
|
|
423
|
+
message: "Project and per-state concurrency limits have available capacity.",
|
|
424
|
+
details: {
|
|
425
|
+
activeRunCount,
|
|
426
|
+
maxConcurrentAgents,
|
|
427
|
+
stateLimit: stateLimit ?? null
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function issueHasBlockingDependency(issue, lifecycle, issues) {
|
|
432
|
+
if (!matchesWorkflowState(issue.state, lifecycle.blockerCheckStates) || issue.blockedBy.length === 0) {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
return unresolvedBlockers(issue, lifecycle, issues).length > 0;
|
|
436
|
+
}
|
|
437
|
+
function unresolvedBlockers(issue, lifecycle, issues) {
|
|
438
|
+
return issue.blockedBy.filter((blockerRef) => {
|
|
439
|
+
if (blockerRef.state && isStateTerminal(blockerRef.state, lifecycle)) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
if (blockerRef.identifier) {
|
|
443
|
+
const blockerIssue = issues.find(
|
|
444
|
+
(candidate) => candidate.identifier === blockerRef.identifier
|
|
445
|
+
);
|
|
446
|
+
if (blockerIssue?.state) {
|
|
447
|
+
return !isStateTerminal(blockerIssue.state, lifecycle);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return true;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
function latestRunForIssue(runs, issueId) {
|
|
454
|
+
return runs.filter((run) => run.issueId === issueId).sort(
|
|
455
|
+
(left, right) => (parseTimestampMs(right.updatedAt) ?? -Infinity) - (parseTimestampMs(left.updatedAt) ?? -Infinity)
|
|
456
|
+
)[0] ?? null;
|
|
457
|
+
}
|
|
458
|
+
function parseTimestampMs(value) {
|
|
459
|
+
if (!value) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const parsed = Date.parse(value);
|
|
463
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
464
|
+
}
|
|
465
|
+
function parseIssueIdentifier(identifier) {
|
|
466
|
+
const match = identifier.match(/^([^/\s#]+)\/([^/\s#]+)#(\d+)$/);
|
|
467
|
+
if (!match) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
owner: match[1],
|
|
472
|
+
name: match[2],
|
|
473
|
+
number: Number.parseInt(match[3], 10)
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function normalizeIdentifier(value) {
|
|
477
|
+
return value.trim().toLowerCase();
|
|
478
|
+
}
|
|
30
479
|
|
|
31
|
-
// ../orchestrator/
|
|
480
|
+
// ../orchestrator/src/service.ts
|
|
32
481
|
import { mkdir as mkdir3, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
33
482
|
import { createWriteStream, mkdirSync } from "fs";
|
|
34
483
|
import { spawn as spawn2 } from "child_process";
|
|
35
|
-
import { join as join3 } from "path";
|
|
484
|
+
import { isAbsolute, join as join3 } from "path";
|
|
36
485
|
import { StringDecoder } from "string_decoder";
|
|
37
486
|
import { fileURLToPath } from "url";
|
|
38
487
|
|
|
39
|
-
// ../orchestrator/
|
|
488
|
+
// ../orchestrator/src/git.ts
|
|
40
489
|
import { spawn } from "child_process";
|
|
41
490
|
import { randomUUID } from "crypto";
|
|
42
|
-
import {
|
|
491
|
+
import {
|
|
492
|
+
access,
|
|
493
|
+
mkdir,
|
|
494
|
+
readFile as readFile2,
|
|
495
|
+
rename,
|
|
496
|
+
rm,
|
|
497
|
+
stat,
|
|
498
|
+
writeFile
|
|
499
|
+
} from "fs/promises";
|
|
43
500
|
import { constants } from "fs";
|
|
44
501
|
import { join } from "path";
|
|
45
502
|
var workflowConfigStore = new WorkflowConfigStore();
|
|
@@ -81,7 +538,10 @@ async function syncRepositoryForRun(input) {
|
|
|
81
538
|
} else {
|
|
82
539
|
await rm(repositoryDirectory, { recursive: true, force: true });
|
|
83
540
|
}
|
|
84
|
-
const tempRepositoryDirectory = join(
|
|
541
|
+
const tempRepositoryDirectory = join(
|
|
542
|
+
input.targetDirectory,
|
|
543
|
+
`repository.tmp-${process.pid}-${Date.now()}`
|
|
544
|
+
);
|
|
85
545
|
await rm(tempRepositoryDirectory, { recursive: true, force: true });
|
|
86
546
|
try {
|
|
87
547
|
await runCommand("git", [
|
|
@@ -102,10 +562,13 @@ async function syncRepositoryForRun(input) {
|
|
|
102
562
|
});
|
|
103
563
|
}
|
|
104
564
|
async function ensureIssueWorkspaceRepository(input) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
565
|
+
if (!input.existingWorkspace) {
|
|
566
|
+
return cloneRepositoryForRun({
|
|
567
|
+
repository: input.repository,
|
|
568
|
+
targetDirectory: input.issueWorkspacePath
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return syncExistingIssueWorkspaceRepository(input);
|
|
109
572
|
}
|
|
110
573
|
async function loadRepositoryWorkflow(repositoryDirectory, _repository) {
|
|
111
574
|
const workflowPath = join(repositoryDirectory, "WORKFLOW.md");
|
|
@@ -115,7 +578,10 @@ async function loadRepositoryWorkflow(repositoryDirectory, _repository) {
|
|
|
115
578
|
if (isMissingFileError(error)) {
|
|
116
579
|
return createDefaultWorkflowResolution();
|
|
117
580
|
}
|
|
118
|
-
return createInvalidWorkflowResolution(
|
|
581
|
+
return createInvalidWorkflowResolution(
|
|
582
|
+
workflowPath,
|
|
583
|
+
error instanceof Error ? error.message : "workflow_parse_error"
|
|
584
|
+
);
|
|
119
585
|
}
|
|
120
586
|
}
|
|
121
587
|
function runCommand(command, args) {
|
|
@@ -133,7 +599,11 @@ function runCommand(command, args) {
|
|
|
133
599
|
resolve4();
|
|
134
600
|
return;
|
|
135
601
|
}
|
|
136
|
-
reject(
|
|
602
|
+
reject(
|
|
603
|
+
new Error(
|
|
604
|
+
stderr.trim() || `${command} exited with code ${code ?? "unknown"}`
|
|
605
|
+
)
|
|
606
|
+
);
|
|
137
607
|
});
|
|
138
608
|
});
|
|
139
609
|
}
|
|
@@ -149,6 +619,107 @@ async function readGitHead(repositoryDirectory) {
|
|
|
149
619
|
return null;
|
|
150
620
|
}
|
|
151
621
|
}
|
|
622
|
+
async function syncExistingIssueWorkspaceRepository(input) {
|
|
623
|
+
await mkdir(input.issueWorkspacePath, { recursive: true });
|
|
624
|
+
const repositoryDirectory = join(input.issueWorkspacePath, "repository");
|
|
625
|
+
const lockDirectory = join(input.issueWorkspacePath, "repository.lock");
|
|
626
|
+
return withRepositoryLock(lockDirectory, async () => {
|
|
627
|
+
const repositoryExists = await pathExists(repositoryDirectory);
|
|
628
|
+
const hasGit = await pathExists(join(repositoryDirectory, ".git"));
|
|
629
|
+
if (hasGit) {
|
|
630
|
+
let dirtyStatus;
|
|
631
|
+
try {
|
|
632
|
+
dirtyStatus = await readGitStatusPorcelain(repositoryDirectory);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw createIssueWorkspacePreservedError(
|
|
635
|
+
repositoryDirectory,
|
|
636
|
+
`could not be inspected: ${formatCommandError(error, "git status --porcelain failed")}`
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (dirtyStatus.trim()) {
|
|
640
|
+
throw createIssueWorkspacePreservedError(
|
|
641
|
+
repositoryDirectory,
|
|
642
|
+
`has uncommitted changes: ${summarizeGitStatus(dirtyStatus)}`
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
await runCommand("git", [
|
|
647
|
+
"-C",
|
|
648
|
+
repositoryDirectory,
|
|
649
|
+
"pull",
|
|
650
|
+
"--ff-only"
|
|
651
|
+
]);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
const message = formatCommandError(error, "git pull --ff-only failed");
|
|
654
|
+
throw createIssueWorkspacePreservedError(
|
|
655
|
+
repositoryDirectory,
|
|
656
|
+
`could not be fast-forwarded: ${message}`
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
return repositoryDirectory;
|
|
660
|
+
}
|
|
661
|
+
if (repositoryExists) {
|
|
662
|
+
throw createIssueWorkspacePreservedError(
|
|
663
|
+
repositoryDirectory,
|
|
664
|
+
"exists but is not a git checkout"
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
const tempRepositoryDirectory = join(
|
|
668
|
+
input.issueWorkspacePath,
|
|
669
|
+
`repository.tmp-${process.pid}-${Date.now()}`
|
|
670
|
+
);
|
|
671
|
+
await rm(tempRepositoryDirectory, { recursive: true, force: true });
|
|
672
|
+
try {
|
|
673
|
+
await runCommand("git", [
|
|
674
|
+
"clone",
|
|
675
|
+
"--depth",
|
|
676
|
+
"1",
|
|
677
|
+
input.repository.cloneUrl,
|
|
678
|
+
tempRepositoryDirectory
|
|
679
|
+
]);
|
|
680
|
+
await rename(tempRepositoryDirectory, repositoryDirectory);
|
|
681
|
+
return repositoryDirectory;
|
|
682
|
+
} finally {
|
|
683
|
+
await rm(tempRepositoryDirectory, { recursive: true, force: true });
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
function createIssueWorkspacePreservedError(repositoryDirectory, reason) {
|
|
688
|
+
return new Error(
|
|
689
|
+
[
|
|
690
|
+
`Issue workspace repository at ${repositoryDirectory} was preserved because it ${reason}.`,
|
|
691
|
+
"Resolve or commit the local workspace changes, or run a configured recovery hook, before retrying."
|
|
692
|
+
].join(" ")
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
function formatCommandError(error, fallback) {
|
|
696
|
+
const message = error instanceof Error ? error.message : fallback;
|
|
697
|
+
return normalizeWhitespace(message) || fallback;
|
|
698
|
+
}
|
|
699
|
+
function summarizeGitStatus(status) {
|
|
700
|
+
const lines = status.trim().split(/\r?\n/).map(normalizeWhitespace).filter(Boolean);
|
|
701
|
+
const summary = lines.slice(0, 5).join("; ");
|
|
702
|
+
return lines.length > 5 ? `${summary}; ...` : summary;
|
|
703
|
+
}
|
|
704
|
+
function normalizeWhitespace(value) {
|
|
705
|
+
return value.replace(/\s+/g, " ").trim();
|
|
706
|
+
}
|
|
707
|
+
async function readGitStatusPorcelain(repositoryDirectory) {
|
|
708
|
+
return runCommandCapture("git", [
|
|
709
|
+
"-C",
|
|
710
|
+
repositoryDirectory,
|
|
711
|
+
"status",
|
|
712
|
+
"--porcelain"
|
|
713
|
+
]);
|
|
714
|
+
}
|
|
715
|
+
async function pathExists(path) {
|
|
716
|
+
try {
|
|
717
|
+
await access(path, constants.F_OK);
|
|
718
|
+
return true;
|
|
719
|
+
} catch {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
152
723
|
function runCommandCapture(command, args) {
|
|
153
724
|
return new Promise((resolve4, reject) => {
|
|
154
725
|
const child = spawn(command, args, {
|
|
@@ -168,7 +739,11 @@ function runCommandCapture(command, args) {
|
|
|
168
739
|
resolve4(stdout.trim());
|
|
169
740
|
return;
|
|
170
741
|
}
|
|
171
|
-
reject(
|
|
742
|
+
reject(
|
|
743
|
+
new Error(
|
|
744
|
+
stderr.trim() || `${command} exited with code ${code ?? "unknown"}`
|
|
745
|
+
)
|
|
746
|
+
);
|
|
172
747
|
});
|
|
173
748
|
});
|
|
174
749
|
}
|
|
@@ -186,9 +761,13 @@ async function acquireRepositoryLock(lockDirectory) {
|
|
|
186
761
|
for (; ; ) {
|
|
187
762
|
try {
|
|
188
763
|
await mkdir(lockDirectory);
|
|
189
|
-
await writeFile(
|
|
764
|
+
await writeFile(
|
|
765
|
+
join(lockDirectory, "owner"),
|
|
766
|
+
`${ownerToken}
|
|
190
767
|
${(/* @__PURE__ */ new Date()).toISOString()}
|
|
191
|
-
`,
|
|
768
|
+
`,
|
|
769
|
+
"utf8"
|
|
770
|
+
);
|
|
192
771
|
return ownerToken;
|
|
193
772
|
} catch (error) {
|
|
194
773
|
if (!isAlreadyExistsError(error)) {
|
|
@@ -201,7 +780,9 @@ ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
|
201
780
|
continue;
|
|
202
781
|
}
|
|
203
782
|
if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
|
|
204
|
-
throw new Error(
|
|
783
|
+
throw new Error(
|
|
784
|
+
`Timed out waiting for repository cache lock: ${lockDirectory}`
|
|
785
|
+
);
|
|
205
786
|
}
|
|
206
787
|
await wait(LOCK_RETRY_MS);
|
|
207
788
|
}
|
|
@@ -232,11 +813,13 @@ async function isStaleLock(lockDirectory) {
|
|
|
232
813
|
}
|
|
233
814
|
}
|
|
234
815
|
function isAlreadyExistsError(error) {
|
|
235
|
-
return Boolean(
|
|
816
|
+
return Boolean(
|
|
817
|
+
error && typeof error === "object" && "code" in error && error.code === "EEXIST"
|
|
818
|
+
);
|
|
236
819
|
}
|
|
237
820
|
async function readLockOwner(lockDirectory) {
|
|
238
821
|
await access(join(lockDirectory, "owner"), constants.R_OK);
|
|
239
|
-
const owner = await
|
|
822
|
+
const owner = await readFile2(join(lockDirectory, "owner"), "utf8");
|
|
240
823
|
return owner.split("\n", 1)[0] || null;
|
|
241
824
|
}
|
|
242
825
|
function wait(ms) {
|
|
@@ -245,41 +828,49 @@ function wait(ms) {
|
|
|
245
828
|
});
|
|
246
829
|
}
|
|
247
830
|
function isMissingFileError(error) {
|
|
248
|
-
return Boolean(
|
|
831
|
+
return Boolean(
|
|
832
|
+
error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
833
|
+
);
|
|
249
834
|
}
|
|
250
835
|
|
|
251
|
-
// ../orchestrator/
|
|
252
|
-
import {
|
|
836
|
+
// ../orchestrator/src/fs-store.ts
|
|
837
|
+
import {
|
|
838
|
+
mkdir as mkdir2,
|
|
839
|
+
open,
|
|
840
|
+
rename as rename2,
|
|
841
|
+
rm as rm2,
|
|
842
|
+
stat as stat2,
|
|
843
|
+
writeFile as writeFile2,
|
|
844
|
+
appendFile
|
|
845
|
+
} from "fs/promises";
|
|
253
846
|
import { dirname, join as join2, relative, resolve } from "path";
|
|
254
847
|
var OrchestratorFsStore = class {
|
|
255
|
-
runtimeRoot;
|
|
256
|
-
resolvedRuntimeRoot;
|
|
257
|
-
resolvedEventsMirrorRoot;
|
|
258
848
|
constructor(runtimeRoot, options = {}) {
|
|
259
849
|
this.runtimeRoot = runtimeRoot;
|
|
260
850
|
this.resolvedRuntimeRoot = resolve(runtimeRoot);
|
|
261
851
|
this.resolvedEventsMirrorRoot = options.eventsMirrorRoot ? resolve(options.eventsMirrorRoot) : null;
|
|
262
852
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return join2(this.projectsRoot(), projectId);
|
|
853
|
+
resolvedRuntimeRoot;
|
|
854
|
+
resolvedEventsMirrorRoot;
|
|
855
|
+
projectDir(_projectId) {
|
|
856
|
+
return this.runtimeRoot;
|
|
268
857
|
}
|
|
269
|
-
|
|
270
|
-
return join2(this.
|
|
858
|
+
runsDir() {
|
|
859
|
+
return join2(this.runtimeRoot, "runs");
|
|
271
860
|
}
|
|
272
|
-
runDir(runId,
|
|
273
|
-
|
|
274
|
-
return join2(this.runtimeRoot, "projects", "__unknown__", "runs", runId);
|
|
275
|
-
}
|
|
276
|
-
return join2(this.projectRunsDir(projectId), runId);
|
|
861
|
+
runDir(runId, _projectId) {
|
|
862
|
+
return join2(this.runsDir(), runId);
|
|
277
863
|
}
|
|
278
864
|
async loadProjectConfig(projectId) {
|
|
279
|
-
return readJsonFile(
|
|
865
|
+
return readJsonFile(
|
|
866
|
+
join2(this.projectDir(projectId), "project.json")
|
|
867
|
+
);
|
|
280
868
|
}
|
|
281
869
|
async saveProjectConfig(config) {
|
|
282
|
-
await writeJsonFile(
|
|
870
|
+
await writeJsonFile(
|
|
871
|
+
join2(this.projectDir(config.projectId), "project.json"),
|
|
872
|
+
config
|
|
873
|
+
);
|
|
283
874
|
}
|
|
284
875
|
async loadProjectIssueOrchestrations(projectId) {
|
|
285
876
|
const issuesPath = join2(this.projectDir(projectId), "issues.json");
|
|
@@ -295,53 +886,74 @@ var OrchestratorFsStore = class {
|
|
|
295
886
|
if (legacyLeases.length === 0) {
|
|
296
887
|
return [];
|
|
297
888
|
}
|
|
298
|
-
const migratedIssues = legacyLeases.map(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
889
|
+
const migratedIssues = legacyLeases.map(
|
|
890
|
+
(lease) => ({
|
|
891
|
+
issueId: lease.issueId,
|
|
892
|
+
identifier: lease.issueIdentifier,
|
|
893
|
+
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(
|
|
894
|
+
lease.issueIdentifier
|
|
895
|
+
),
|
|
896
|
+
completedOnce: false,
|
|
897
|
+
failureRetryCount: 0,
|
|
898
|
+
state: lease.status === "active" ? "claimed" : "released",
|
|
899
|
+
currentRunId: lease.status === "active" ? lease.runId : null,
|
|
900
|
+
retryEntry: null,
|
|
901
|
+
updatedAt: lease.updatedAt
|
|
902
|
+
})
|
|
903
|
+
);
|
|
309
904
|
await this.saveProjectIssueOrchestrations(projectId, migratedIssues);
|
|
310
905
|
return migratedIssues;
|
|
311
906
|
}
|
|
312
907
|
async saveProjectIssueOrchestrations(projectId, issues) {
|
|
313
|
-
await writeJsonFile(
|
|
908
|
+
await writeJsonFile(
|
|
909
|
+
join2(this.projectDir(projectId), "issues.json"),
|
|
910
|
+
issues
|
|
911
|
+
);
|
|
314
912
|
}
|
|
315
913
|
async saveProjectStatus(status) {
|
|
316
|
-
await writeJsonFile(
|
|
914
|
+
await writeJsonFile(
|
|
915
|
+
join2(this.projectDir(), "status.json"),
|
|
916
|
+
status
|
|
917
|
+
);
|
|
317
918
|
}
|
|
318
919
|
async loadProjectStatus(projectId) {
|
|
319
|
-
return await readJsonFile(
|
|
920
|
+
return await readJsonFile(
|
|
921
|
+
join2(this.projectDir(projectId), "status.json")
|
|
922
|
+
) ?? null;
|
|
320
923
|
}
|
|
321
924
|
async loadRun(runId, projectId) {
|
|
322
925
|
const runDirectory = projectId !== void 0 ? this.runDir(runId, projectId) : await this.findRunDir(runId);
|
|
323
926
|
if (!runDirectory) {
|
|
324
927
|
return null;
|
|
325
928
|
}
|
|
326
|
-
return await readJsonFile(
|
|
929
|
+
return await readJsonFile(
|
|
930
|
+
join2(runDirectory, "run.json")
|
|
931
|
+
) ?? null;
|
|
327
932
|
}
|
|
328
933
|
async loadAllRuns() {
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
934
|
+
const runIds = await safeReadDir(this.runsDir());
|
|
935
|
+
const runs = await Promise.all(
|
|
936
|
+
runIds.map(
|
|
937
|
+
(runId) => readJsonFile(
|
|
938
|
+
join2(this.runDir(runId), "run.json")
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
);
|
|
335
942
|
return runs.filter((run) => Boolean(run));
|
|
336
943
|
}
|
|
337
944
|
async saveRun(run) {
|
|
338
|
-
await writeJsonFile(
|
|
945
|
+
await writeJsonFile(
|
|
946
|
+
join2(this.runDir(run.runId, run.projectId), "run.json"),
|
|
947
|
+
run
|
|
948
|
+
);
|
|
339
949
|
}
|
|
340
950
|
async appendRunEvent(runId, event) {
|
|
341
951
|
const resolvedProjectId = "projectId" in event && typeof event.projectId === "string" ? event.projectId : void 0;
|
|
342
952
|
const runDirectory = resolvedProjectId !== void 0 ? this.runDir(runId, resolvedProjectId) : await this.findRunDir(runId);
|
|
343
953
|
if (!runDirectory) {
|
|
344
|
-
throw new Error(
|
|
954
|
+
throw new Error(
|
|
955
|
+
`Unable to resolve run directory for event append: ${runId}`
|
|
956
|
+
);
|
|
345
957
|
}
|
|
346
958
|
const path = join2(runDirectory, "events.ndjson");
|
|
347
959
|
const resolvedPath = resolve(path);
|
|
@@ -362,7 +974,9 @@ var OrchestratorFsStore = class {
|
|
|
362
974
|
mode: 420
|
|
363
975
|
});
|
|
364
976
|
} catch (error) {
|
|
365
|
-
console.warn(
|
|
977
|
+
console.warn(
|
|
978
|
+
`Failed to mirror orchestrator event log to ${mirrorPath}: ${error instanceof Error ? error.message : String(error)}`
|
|
979
|
+
);
|
|
366
980
|
}
|
|
367
981
|
}
|
|
368
982
|
async loadRecentRunEvents(runId, limit = 20, projectId) {
|
|
@@ -387,999 +1001,111 @@ var OrchestratorFsStore = class {
|
|
|
387
1001
|
await handle.read(chunk, 0, readSize, position);
|
|
388
1002
|
tail = Buffer.concat([chunk, tail]);
|
|
389
1003
|
const events = parseRecentEvents(tail.toString("utf8"), limit, {
|
|
390
|
-
allowPartialFirstLine: position > 0
|
|
391
|
-
});
|
|
392
|
-
if (events.length >= limit) {
|
|
393
|
-
return events;
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
return parseRecentEvents(tail.toString("utf8"), limit, {
|
|
397
|
-
allowPartialFirstLine: false
|
|
398
|
-
});
|
|
399
|
-
} finally {
|
|
400
|
-
await handle.close();
|
|
401
|
-
}
|
|
402
|
-
} catch (error) {
|
|
403
|
-
if (isFileMissing(error)) {
|
|
404
|
-
return [];
|
|
405
|
-
}
|
|
406
|
-
throw error;
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
issueWorkspaceDir(projectId, workspaceKey) {
|
|
410
|
-
return join2(this.projectDir(projectId), "issues", workspaceKey);
|
|
411
|
-
}
|
|
412
|
-
async loadIssueWorkspace(projectId, workspaceKey) {
|
|
413
|
-
return await readJsonFile(join2(this.issueWorkspaceDir(projectId, workspaceKey), "workspace.json")) ?? null;
|
|
414
|
-
}
|
|
415
|
-
async loadIssueWorkspaces(projectId) {
|
|
416
|
-
const issuesDir = join2(this.projectDir(projectId), "issues");
|
|
417
|
-
const entries = await safeReadDir(issuesDir);
|
|
418
|
-
const records = await Promise.all(entries.map((entry) => this.loadIssueWorkspace(projectId, entry)));
|
|
419
|
-
return records.filter((record) => Boolean(record));
|
|
420
|
-
}
|
|
421
|
-
async saveIssueWorkspace(record) {
|
|
422
|
-
await writeJsonFile(join2(this.issueWorkspaceDir(record.projectId, record.workspaceKey), "workspace.json"), record);
|
|
423
|
-
}
|
|
424
|
-
async removeIssueWorkspace(projectId, workspaceKey) {
|
|
425
|
-
const dir = this.issueWorkspaceDir(projectId, workspaceKey);
|
|
426
|
-
await rm2(dir, { recursive: true, force: true });
|
|
427
|
-
}
|
|
428
|
-
async findRunDir(runId) {
|
|
429
|
-
const projectIds = await safeReadDir(this.projectsRoot());
|
|
430
|
-
for (const projectId of projectIds) {
|
|
431
|
-
const candidate = this.runDir(runId, projectId);
|
|
432
|
-
const run = await readJsonFile(join2(candidate, "run.json"));
|
|
433
|
-
if (run || await pathExists(join2(candidate, "events.ndjson"))) {
|
|
434
|
-
return candidate;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
return null;
|
|
438
|
-
}
|
|
439
|
-
resolveMirroredEventsPath(primaryPath) {
|
|
440
|
-
if (!this.resolvedEventsMirrorRoot) {
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
const relativePath = relative(this.resolvedRuntimeRoot, primaryPath);
|
|
444
|
-
if (relativePath.startsWith("..")) {
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
const mirrorPath = join2(this.resolvedEventsMirrorRoot, relativePath);
|
|
448
|
-
return mirrorPath === primaryPath ? null : mirrorPath;
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
async function writeJsonFile(path, value) {
|
|
452
|
-
await mkdir2(dirname(path), { recursive: true });
|
|
453
|
-
const temporaryPath = `${path}.tmp`;
|
|
454
|
-
await writeFile2(temporaryPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
455
|
-
await rename2(temporaryPath, path);
|
|
456
|
-
}
|
|
457
|
-
async function pathExists(path) {
|
|
458
|
-
try {
|
|
459
|
-
await stat2(path);
|
|
460
|
-
return true;
|
|
461
|
-
} catch (error) {
|
|
462
|
-
if (isFileMissing(error)) {
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
465
|
-
throw error;
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// ../tracker-github/dist/adapter.js
|
|
470
|
-
import { createHash } from "crypto";
|
|
471
|
-
var DEFAULT_API_URL = "https://api.github.com/graphql";
|
|
472
|
-
var DEFAULT_PAGE_SIZE = 25;
|
|
473
|
-
var DEFAULT_NETWORK_TIMEOUT_MS = 3e4;
|
|
474
|
-
var RATE_LIMIT_THRESHOLD = 100;
|
|
475
|
-
var MAX_RATE_LIMIT_WAIT_MS = 6e4;
|
|
476
|
-
var GitHubTrackerError = class extends Error {
|
|
477
|
-
};
|
|
478
|
-
var GitHubTrackerHttpError = class extends GitHubTrackerError {
|
|
479
|
-
status;
|
|
480
|
-
details;
|
|
481
|
-
constructor(message, status, details) {
|
|
482
|
-
super(message);
|
|
483
|
-
this.status = status;
|
|
484
|
-
this.details = details;
|
|
485
|
-
}
|
|
486
|
-
};
|
|
487
|
-
var GitHubTrackerQueryError = class extends GitHubTrackerError {
|
|
488
|
-
};
|
|
489
|
-
var cachedGitHubGraphQLRateLimits = /* @__PURE__ */ new Map();
|
|
490
|
-
function normalizeProjectItem(projectId, item, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, priority = {}, rateLimits = null) {
|
|
491
|
-
if (item.content?.__typename !== "Issue") {
|
|
492
|
-
return null;
|
|
493
|
-
}
|
|
494
|
-
const fieldValues = extractFieldValues(item.fieldValues?.nodes ?? []);
|
|
495
|
-
const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
|
|
496
|
-
const repository = item.content.repository;
|
|
497
|
-
const blockedBy = (item.content.blockedBy?.nodes ?? []).flatMap((node) => node ? [
|
|
498
|
-
{
|
|
499
|
-
id: node.id,
|
|
500
|
-
identifier: `${node.repository.owner.login}/${node.repository.name}#${node.number}`,
|
|
501
|
-
state: normalizeBlockerState(node.state, lifecycle)
|
|
502
|
-
}
|
|
503
|
-
] : []);
|
|
504
|
-
const issueUpdatedAtMs = parseTimestampMs(item.content.updatedAt);
|
|
505
|
-
const itemUpdatedAtMs = parseTimestampMs(item.updatedAt);
|
|
506
|
-
const trackedUpdatedAt = itemUpdatedAtMs !== null && (issueUpdatedAtMs === null || itemUpdatedAtMs > issueUpdatedAtMs) ? item.updatedAt : item.content.updatedAt ?? item.updatedAt;
|
|
507
|
-
return {
|
|
508
|
-
id: item.content.id,
|
|
509
|
-
identifier: `${repository.owner.login}/${repository.name}#${item.content.number}`,
|
|
510
|
-
number: item.content.number,
|
|
511
|
-
title: item.content.title,
|
|
512
|
-
description: item.content.body,
|
|
513
|
-
priority: resolvePriority(item, priority),
|
|
514
|
-
state,
|
|
515
|
-
branchName: null,
|
|
516
|
-
url: item.content.url,
|
|
517
|
-
labels: (item.content.labels?.nodes ?? []).flatMap((label) => label?.name ? [label.name.toLowerCase()] : []).sort(),
|
|
518
|
-
blockedBy,
|
|
519
|
-
createdAt: item.content.createdAt,
|
|
520
|
-
updatedAt: trackedUpdatedAt,
|
|
521
|
-
repository: {
|
|
522
|
-
owner: repository.owner.login,
|
|
523
|
-
name: repository.name,
|
|
524
|
-
url: repository.url,
|
|
525
|
-
cloneUrl: deriveCloneUrl(repository.url)
|
|
526
|
-
},
|
|
527
|
-
tracker: {
|
|
528
|
-
adapter: "github-project",
|
|
529
|
-
bindingId: projectId,
|
|
530
|
-
itemId: item.id
|
|
531
|
-
},
|
|
532
|
-
metadata: fieldValues,
|
|
533
|
-
rateLimits
|
|
534
|
-
};
|
|
535
|
-
}
|
|
536
|
-
async function fetchProjectIssues(config, fetchImpl = fetch) {
|
|
537
|
-
const issues = [];
|
|
538
|
-
let cursor = null;
|
|
539
|
-
const priorityOptionIds = config.priorityFieldName ? await fetchPriorityOptionOrder(config, config.priorityFieldName, fetchImpl) : void 0;
|
|
540
|
-
const currentUserLogin = config.assignedOnly ? await fetchCurrentUserLogin(config, fetchImpl) : null;
|
|
541
|
-
let excludedCount = 0;
|
|
542
|
-
let latestRateLimits = null;
|
|
543
|
-
do {
|
|
544
|
-
const pageResult = await fetchProjectItemsPage(config, cursor, fetchImpl);
|
|
545
|
-
const page = pageResult.page;
|
|
546
|
-
latestRateLimits = pageResult.rateLimits ?? latestRateLimits;
|
|
547
|
-
const pageIssues = (page.nodes ?? []).flatMap((item) => {
|
|
548
|
-
if (!item) {
|
|
549
|
-
return [];
|
|
550
|
-
}
|
|
551
|
-
const normalized = normalizeProjectItem(config.projectId, item, config.lifecycle, {
|
|
552
|
-
fieldName: config.priorityFieldName,
|
|
553
|
-
optionIds: priorityOptionIds
|
|
554
|
-
}, latestRateLimits);
|
|
555
|
-
if (!normalized) {
|
|
556
|
-
return [];
|
|
557
|
-
}
|
|
558
|
-
if (currentUserLogin && !isIssueAssignedToLogin(item, currentUserLogin)) {
|
|
559
|
-
excludedCount += 1;
|
|
560
|
-
return [];
|
|
561
|
-
}
|
|
562
|
-
return [normalized];
|
|
563
|
-
});
|
|
564
|
-
issues.push(...pageIssues);
|
|
565
|
-
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
566
|
-
} while (cursor);
|
|
567
|
-
if (currentUserLogin) {
|
|
568
|
-
emitAssignedOnlyFilterEvent({
|
|
569
|
-
projectId: config.projectId,
|
|
570
|
-
currentUserLogin,
|
|
571
|
-
includedCount: issues.length,
|
|
572
|
-
excludedCount
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
if (latestRateLimits) {
|
|
576
|
-
for (const issue of issues) {
|
|
577
|
-
issue.rateLimits = latestRateLimits;
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
return issues;
|
|
581
|
-
}
|
|
582
|
-
async function fetchIssueStatesByIds(config, issueIds, fetchImpl = fetch) {
|
|
583
|
-
if (issueIds.length === 0) {
|
|
584
|
-
return [];
|
|
585
|
-
}
|
|
586
|
-
const issues = [];
|
|
587
|
-
for (const issueIdBatch of chunkValues([...new Set(issueIds)], 100)) {
|
|
588
|
-
const result = await executeGraphQLQueryWithMetadata(config, ISSUE_STATES_BY_IDS_QUERY, {
|
|
589
|
-
issueIds: issueIdBatch
|
|
590
|
-
}, fetchImpl);
|
|
591
|
-
const data = result.data;
|
|
592
|
-
const rateLimits = result.rateLimits;
|
|
593
|
-
for (const node of data.nodes ?? []) {
|
|
594
|
-
const projectItem = await resolveIssueProjectItemForStateLookup(config, node, fetchImpl);
|
|
595
|
-
const normalized = normalizeIssueStateLookupNode(config.projectId, node, projectItem, config.lifecycle, rateLimits);
|
|
596
|
-
if (normalized) {
|
|
597
|
-
issues.push(normalized);
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return issues;
|
|
602
|
-
}
|
|
603
|
-
async function fetchProjectItemsPage(config, cursor, fetchImpl) {
|
|
604
|
-
const result = await executeGraphQLQueryWithMetadata(config, PROJECT_ITEMS_QUERY, {
|
|
605
|
-
projectId: config.projectId,
|
|
606
|
-
cursor,
|
|
607
|
-
pageSize: config.pageSize ?? DEFAULT_PAGE_SIZE
|
|
608
|
-
}, fetchImpl);
|
|
609
|
-
const data = result.data;
|
|
610
|
-
const items = data.node?.items;
|
|
611
|
-
if (!items) {
|
|
612
|
-
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include project items.");
|
|
613
|
-
}
|
|
614
|
-
return {
|
|
615
|
-
page: items,
|
|
616
|
-
rateLimits: result.rateLimits
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
var fetchGithubProjectIssues = fetchProjectIssues;
|
|
620
|
-
var fetchGithubIssueStatesByIds = fetchIssueStatesByIds;
|
|
621
|
-
async function fetchCurrentUserLogin(config, fetchImpl) {
|
|
622
|
-
const response = await fetchImpl(resolveRestUserApiUrl(config.apiUrl), {
|
|
623
|
-
method: "GET",
|
|
624
|
-
headers: {
|
|
625
|
-
authorization: `Bearer ${config.token}`,
|
|
626
|
-
"user-agent": "gh-symphony",
|
|
627
|
-
accept: "application/vnd.github+json"
|
|
628
|
-
},
|
|
629
|
-
signal: buildRequestSignal(config.timeoutMs)
|
|
630
|
-
});
|
|
631
|
-
if (!response.ok) {
|
|
632
|
-
const details = await response.text();
|
|
633
|
-
throw new GitHubTrackerHttpError(`GitHub REST request failed with status ${response.status}`, response.status, details);
|
|
634
|
-
}
|
|
635
|
-
const payload = await response.json();
|
|
636
|
-
if (!payload.login) {
|
|
637
|
-
throw new GitHubTrackerQueryError("GitHub REST response did not include the authenticated user login.");
|
|
638
|
-
}
|
|
639
|
-
return payload.login;
|
|
640
|
-
}
|
|
641
|
-
function isIssueAssignedToLogin(item, login) {
|
|
642
|
-
if (item.content?.__typename !== "Issue") {
|
|
643
|
-
return false;
|
|
644
|
-
}
|
|
645
|
-
return (item.content.assignees?.nodes ?? []).some((assignee) => assignee?.login === login);
|
|
646
|
-
}
|
|
647
|
-
function emitAssignedOnlyFilterEvent(input) {
|
|
648
|
-
console.info(JSON.stringify({
|
|
649
|
-
event: "tracker-assigned-only-filtered",
|
|
650
|
-
projectId: input.projectId,
|
|
651
|
-
currentUserLogin: input.currentUserLogin,
|
|
652
|
-
includedCount: input.includedCount,
|
|
653
|
-
excludedCount: input.excludedCount
|
|
654
|
-
}));
|
|
655
|
-
}
|
|
656
|
-
function extractFieldValues(nodes) {
|
|
657
|
-
return nodes.reduce((values, node) => {
|
|
658
|
-
const fieldName = node?.field?.name;
|
|
659
|
-
if (!fieldName) {
|
|
660
|
-
return values;
|
|
661
|
-
}
|
|
662
|
-
if (node.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.name) {
|
|
663
|
-
values[fieldName] = node.name;
|
|
664
|
-
}
|
|
665
|
-
if (node.__typename === "ProjectV2ItemFieldTextValue" && node.text) {
|
|
666
|
-
values[fieldName] = node.text;
|
|
667
|
-
}
|
|
668
|
-
return values;
|
|
669
|
-
}, {});
|
|
670
|
-
}
|
|
671
|
-
function normalizeIssueStateLookupNode(projectId, issue, projectItem, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, rateLimits = null) {
|
|
672
|
-
if (issue?.__typename !== "Issue") {
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
if (!projectItem) {
|
|
676
|
-
return null;
|
|
677
|
-
}
|
|
678
|
-
const fieldValues = extractFieldValues(projectItem.fieldValues?.nodes ?? []);
|
|
679
|
-
const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
|
|
680
|
-
const repository = issue.repository;
|
|
681
|
-
const identifier = `${repository.owner.login}/${repository.name}#${issue.number}`;
|
|
682
|
-
return {
|
|
683
|
-
id: issue.id,
|
|
684
|
-
identifier,
|
|
685
|
-
number: issue.number,
|
|
686
|
-
title: identifier,
|
|
687
|
-
description: null,
|
|
688
|
-
priority: null,
|
|
689
|
-
state,
|
|
690
|
-
branchName: null,
|
|
691
|
-
url: `${repository.url}/issues/${issue.number}`,
|
|
692
|
-
labels: [],
|
|
693
|
-
blockedBy: [],
|
|
694
|
-
createdAt: null,
|
|
695
|
-
updatedAt: projectItem.updatedAt ?? issue.updatedAt,
|
|
696
|
-
repository: {
|
|
697
|
-
owner: repository.owner.login,
|
|
698
|
-
name: repository.name,
|
|
699
|
-
url: repository.url,
|
|
700
|
-
cloneUrl: deriveCloneUrl(repository.url)
|
|
701
|
-
},
|
|
702
|
-
tracker: {
|
|
703
|
-
adapter: "github-project",
|
|
704
|
-
bindingId: projectId,
|
|
705
|
-
itemId: projectItem.id
|
|
706
|
-
},
|
|
707
|
-
metadata: fieldValues,
|
|
708
|
-
rateLimits
|
|
709
|
-
};
|
|
710
|
-
}
|
|
711
|
-
async function resolveIssueProjectItemForStateLookup(config, issue, fetchImpl) {
|
|
712
|
-
if (issue?.__typename !== "Issue") {
|
|
713
|
-
return null;
|
|
714
|
-
}
|
|
715
|
-
let connection = issue.projectItems;
|
|
716
|
-
let projectItem = findProjectItemByProjectId(connection?.nodes ?? [], config.projectId);
|
|
717
|
-
let cursor = connection?.pageInfo.endCursor ?? null;
|
|
718
|
-
while (!projectItem && connection?.pageInfo.hasNextPage) {
|
|
719
|
-
const nextPage = await fetchIssueProjectItemsPage(config, issue.id, cursor, fetchImpl);
|
|
720
|
-
projectItem = findProjectItemByProjectId(nextPage.nodes ?? [], config.projectId);
|
|
721
|
-
connection = nextPage;
|
|
722
|
-
cursor = nextPage.pageInfo.endCursor;
|
|
723
|
-
}
|
|
724
|
-
return projectItem;
|
|
725
|
-
}
|
|
726
|
-
async function fetchIssueProjectItemsPage(config, issueId, cursor, fetchImpl) {
|
|
727
|
-
const result = await executeGraphQLQueryWithMetadata(config, ISSUE_PROJECT_ITEMS_PAGE_QUERY, {
|
|
728
|
-
issueId,
|
|
729
|
-
cursor
|
|
730
|
-
}, fetchImpl);
|
|
731
|
-
const data = result.data;
|
|
732
|
-
const issue = data.node;
|
|
733
|
-
if (issue?.__typename !== "Issue" || !issue.projectItems) {
|
|
734
|
-
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include issue project items.");
|
|
735
|
-
}
|
|
736
|
-
return issue.projectItems;
|
|
737
|
-
}
|
|
738
|
-
function findProjectItemByProjectId(nodes, projectId) {
|
|
739
|
-
return nodes.find((item) => item?.project?.id === projectId) ?? null;
|
|
740
|
-
}
|
|
741
|
-
function resolvePriority(item, priority) {
|
|
742
|
-
if (!priority.fieldName || !priority.optionIds) {
|
|
743
|
-
return null;
|
|
744
|
-
}
|
|
745
|
-
for (const node of item.fieldValues?.nodes ?? []) {
|
|
746
|
-
if (node?.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.field?.name === priority.fieldName && node.optionId) {
|
|
747
|
-
return priority.optionIds[node.optionId] ?? null;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
return null;
|
|
751
|
-
}
|
|
752
|
-
function extractPriorityOptionOrder(fields, priorityFieldName) {
|
|
753
|
-
for (const field of fields) {
|
|
754
|
-
if (isSingleSelectProjectField(field) && field.name === priorityFieldName) {
|
|
755
|
-
let nextPriority = 0;
|
|
756
|
-
const optionEntries = (field.options ?? []).flatMap((option) => {
|
|
757
|
-
if (!option?.id) {
|
|
758
|
-
return [];
|
|
759
|
-
}
|
|
760
|
-
const entry = [option.id, nextPriority];
|
|
761
|
-
nextPriority += 1;
|
|
762
|
-
return [entry];
|
|
763
|
-
});
|
|
764
|
-
return Object.fromEntries(optionEntries);
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
return void 0;
|
|
768
|
-
}
|
|
769
|
-
async function fetchPriorityOptionOrder(config, priorityFieldName, fetchImpl) {
|
|
770
|
-
const data = await executeGraphQLQuery(config, PROJECT_FIELDS_QUERY, { projectId: config.projectId }, fetchImpl);
|
|
771
|
-
return extractPriorityOptionOrder(data.node?.fields?.nodes ?? [], priorityFieldName);
|
|
772
|
-
}
|
|
773
|
-
function isSingleSelectProjectField(field) {
|
|
774
|
-
return field?.__typename === "ProjectV2SingleSelectField";
|
|
775
|
-
}
|
|
776
|
-
function deriveCloneUrl(repositoryUrl) {
|
|
777
|
-
if (repositoryUrl.startsWith("file://") || repositoryUrl.endsWith(".git")) {
|
|
778
|
-
return repositoryUrl;
|
|
779
|
-
}
|
|
780
|
-
return `${repositoryUrl}.git`;
|
|
781
|
-
}
|
|
782
|
-
function normalizeBlockerState(state, lifecycle) {
|
|
783
|
-
if (!state) {
|
|
784
|
-
return null;
|
|
785
|
-
}
|
|
786
|
-
const normalized = state.trim().toLowerCase();
|
|
787
|
-
if (normalized === "closed") {
|
|
788
|
-
return lifecycle.terminalStates[0] ?? state;
|
|
789
|
-
}
|
|
790
|
-
if (normalized === "open") {
|
|
791
|
-
return null;
|
|
792
|
-
}
|
|
793
|
-
return state;
|
|
794
|
-
}
|
|
795
|
-
function resolveRestUserApiUrl(apiUrl) {
|
|
796
|
-
const parsed = new URL(apiUrl ?? DEFAULT_API_URL);
|
|
797
|
-
const pathSegments = parsed.pathname.split("/").filter(Boolean);
|
|
798
|
-
if (pathSegments.at(-1) === "graphql") {
|
|
799
|
-
pathSegments.pop();
|
|
800
|
-
}
|
|
801
|
-
parsed.pathname = `/${pathSegments.join("/")}/user`.replace(/\/{2,}/g, "/");
|
|
802
|
-
parsed.search = "";
|
|
803
|
-
parsed.hash = "";
|
|
804
|
-
return parsed.toString();
|
|
805
|
-
}
|
|
806
|
-
function chunkValues(values, size) {
|
|
807
|
-
const chunks = [];
|
|
808
|
-
for (let index = 0; index < values.length; index += size) {
|
|
809
|
-
chunks.push(values.slice(index, index + size));
|
|
810
|
-
}
|
|
811
|
-
return chunks;
|
|
812
|
-
}
|
|
813
|
-
function buildRequestSignal(timeoutMs) {
|
|
814
|
-
return AbortSignal.timeout(resolveNetworkTimeoutMs(timeoutMs));
|
|
815
|
-
}
|
|
816
|
-
function resolveNetworkTimeoutMs(timeoutMs) {
|
|
817
|
-
if (timeoutMs !== void 0 && Number.isInteger(timeoutMs) && timeoutMs > 0) {
|
|
818
|
-
return timeoutMs;
|
|
819
|
-
}
|
|
820
|
-
return DEFAULT_NETWORK_TIMEOUT_MS;
|
|
821
|
-
}
|
|
822
|
-
async function executeGraphQLQuery(config, query, variables, fetchImpl) {
|
|
823
|
-
const result = await executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl);
|
|
824
|
-
return result.data;
|
|
825
|
-
}
|
|
826
|
-
async function executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl) {
|
|
827
|
-
const tokenFingerprint = fingerprintToken(config.token);
|
|
828
|
-
await guardGraphQLRateLimit(tokenFingerprint);
|
|
829
|
-
const response = await fetchImpl(config.apiUrl ?? DEFAULT_API_URL, {
|
|
830
|
-
method: "POST",
|
|
831
|
-
headers: {
|
|
832
|
-
"content-type": "application/json",
|
|
833
|
-
authorization: `Bearer ${config.token}`
|
|
834
|
-
},
|
|
835
|
-
body: JSON.stringify({
|
|
836
|
-
query,
|
|
837
|
-
variables
|
|
838
|
-
}),
|
|
839
|
-
signal: buildRequestSignal(config.timeoutMs)
|
|
840
|
-
});
|
|
841
|
-
if (!response.ok) {
|
|
842
|
-
const details = await response.text();
|
|
843
|
-
throw new GitHubTrackerHttpError(`GitHub GraphQL request failed with status ${response.status}`, response.status, details);
|
|
844
|
-
}
|
|
845
|
-
const payload = await response.json();
|
|
846
|
-
if (payload.errors?.length) {
|
|
847
|
-
throw new GitHubTrackerQueryError(payload.errors.map((error) => error.message).join("; "));
|
|
848
|
-
}
|
|
849
|
-
if (!payload.data) {
|
|
850
|
-
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include data.");
|
|
851
|
-
}
|
|
852
|
-
const data = payload.data;
|
|
853
|
-
const rateLimits = extractGitHubRateLimits(response.headers);
|
|
854
|
-
cachedGitHubGraphQLRateLimits.set(tokenFingerprint, rateLimits);
|
|
855
|
-
return {
|
|
856
|
-
data,
|
|
857
|
-
rateLimits
|
|
858
|
-
};
|
|
859
|
-
}
|
|
860
|
-
async function guardGraphQLRateLimit(tokenFingerprint) {
|
|
861
|
-
const rateLimit = cachedGitHubGraphQLRateLimits.get(tokenFingerprint) ?? null;
|
|
862
|
-
if (!rateLimit) {
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
const remaining = rateLimit.remaining;
|
|
866
|
-
if (remaining === null || remaining > RATE_LIMIT_THRESHOLD) {
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
const resetAtMs = parseTimestampMs(rateLimit.resetAt);
|
|
870
|
-
if (resetAtMs === null) {
|
|
871
|
-
throw new GitHubTrackerError("Rate limit near exhaustion");
|
|
872
|
-
}
|
|
873
|
-
const waitMs = Math.max(0, resetAtMs - Date.now());
|
|
874
|
-
if (waitMs > MAX_RATE_LIMIT_WAIT_MS) {
|
|
875
|
-
throw new GitHubTrackerError("Rate limit near exhaustion");
|
|
876
|
-
}
|
|
877
|
-
cachedGitHubGraphQLRateLimits.delete(tokenFingerprint);
|
|
878
|
-
if (waitMs > 0) {
|
|
879
|
-
await sleep(waitMs);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
function fingerprintToken(token) {
|
|
883
|
-
return createHash("sha256").update(token).digest("hex");
|
|
884
|
-
}
|
|
885
|
-
function extractGitHubRateLimits(headers) {
|
|
886
|
-
if (!headers || typeof headers.get !== "function") {
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
const limit = parseIntegerHeader(headers.get("x-ratelimit-limit"));
|
|
890
|
-
const remaining = parseIntegerHeader(headers.get("x-ratelimit-remaining"));
|
|
891
|
-
const used = parseIntegerHeader(headers.get("x-ratelimit-used"));
|
|
892
|
-
const reset = parseIntegerHeader(headers.get("x-ratelimit-reset"));
|
|
893
|
-
const resource = headers.get("x-ratelimit-resource");
|
|
894
|
-
if (limit === null && remaining === null && used === null && reset === null && resource === null) {
|
|
895
|
-
return null;
|
|
896
|
-
}
|
|
897
|
-
return {
|
|
898
|
-
source: "github",
|
|
899
|
-
limit,
|
|
900
|
-
remaining,
|
|
901
|
-
used,
|
|
902
|
-
reset,
|
|
903
|
-
resetAt: reset === null ? null : new Date(reset * 1e3).toISOString(),
|
|
904
|
-
resource
|
|
905
|
-
};
|
|
906
|
-
}
|
|
907
|
-
function parseIntegerHeader(value) {
|
|
908
|
-
if (value === null) {
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
const parsed = Number.parseInt(value, 10);
|
|
912
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
913
|
-
}
|
|
914
|
-
function parseTimestampMs(value) {
|
|
915
|
-
if (!value) {
|
|
916
|
-
return null;
|
|
917
|
-
}
|
|
918
|
-
const timestampMs = Date.parse(value);
|
|
919
|
-
return Number.isFinite(timestampMs) ? timestampMs : null;
|
|
920
|
-
}
|
|
921
|
-
function sleep(ms) {
|
|
922
|
-
return new Promise((resolve4) => {
|
|
923
|
-
setTimeout(resolve4, ms);
|
|
924
|
-
});
|
|
925
|
-
}
|
|
926
|
-
var PROJECT_ITEMS_QUERY = `
|
|
927
|
-
query ProjectItems($projectId: ID!, $cursor: String, $pageSize: Int!) {
|
|
928
|
-
node(id: $projectId) {
|
|
929
|
-
__typename
|
|
930
|
-
... on ProjectV2 {
|
|
931
|
-
items(first: $pageSize, after: $cursor) {
|
|
932
|
-
nodes {
|
|
933
|
-
id
|
|
934
|
-
updatedAt
|
|
935
|
-
fieldValues(first: 20) {
|
|
936
|
-
nodes {
|
|
937
|
-
__typename
|
|
938
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
939
|
-
name
|
|
940
|
-
optionId
|
|
941
|
-
field {
|
|
942
|
-
... on ProjectV2SingleSelectField {
|
|
943
|
-
name
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
... on ProjectV2ItemFieldTextValue {
|
|
948
|
-
text
|
|
949
|
-
field {
|
|
950
|
-
... on ProjectV2FieldCommon {
|
|
951
|
-
name
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
content {
|
|
958
|
-
__typename
|
|
959
|
-
... on Issue {
|
|
960
|
-
id
|
|
961
|
-
number
|
|
962
|
-
title
|
|
963
|
-
body
|
|
964
|
-
url
|
|
965
|
-
createdAt
|
|
966
|
-
updatedAt
|
|
967
|
-
labels(first: 20) {
|
|
968
|
-
nodes {
|
|
969
|
-
name
|
|
970
|
-
}
|
|
971
|
-
}
|
|
972
|
-
assignees(first: 20) {
|
|
973
|
-
nodes {
|
|
974
|
-
login
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
repository {
|
|
978
|
-
name
|
|
979
|
-
url
|
|
980
|
-
owner {
|
|
981
|
-
login
|
|
982
|
-
}
|
|
983
|
-
}
|
|
984
|
-
blockedBy(first: 100) {
|
|
985
|
-
nodes {
|
|
986
|
-
id
|
|
987
|
-
number
|
|
988
|
-
state
|
|
989
|
-
repository {
|
|
990
|
-
name
|
|
991
|
-
owner {
|
|
992
|
-
login
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
pageInfo {
|
|
1001
|
-
endCursor
|
|
1002
|
-
hasNextPage
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
`;
|
|
1009
|
-
var PROJECT_FIELDS_QUERY = `
|
|
1010
|
-
query ProjectFields($projectId: ID!) {
|
|
1011
|
-
node(id: $projectId) {
|
|
1012
|
-
__typename
|
|
1013
|
-
... on ProjectV2 {
|
|
1014
|
-
fields(first: 100) {
|
|
1015
|
-
nodes {
|
|
1016
|
-
__typename
|
|
1017
|
-
... on ProjectV2SingleSelectField {
|
|
1018
|
-
name
|
|
1019
|
-
options {
|
|
1020
|
-
id
|
|
1021
|
-
name
|
|
1022
|
-
}
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
`;
|
|
1030
|
-
var ISSUE_STATES_BY_IDS_QUERY = `
|
|
1031
|
-
query IssueStatesByIds($issueIds: [ID!]!) {
|
|
1032
|
-
nodes(ids: $issueIds) {
|
|
1033
|
-
__typename
|
|
1034
|
-
... on Issue {
|
|
1035
|
-
id
|
|
1036
|
-
number
|
|
1037
|
-
updatedAt
|
|
1038
|
-
repository {
|
|
1039
|
-
name
|
|
1040
|
-
url
|
|
1041
|
-
owner {
|
|
1042
|
-
login
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
projectItems(first: 100, includeArchived: false) {
|
|
1046
|
-
nodes {
|
|
1047
|
-
id
|
|
1048
|
-
updatedAt
|
|
1049
|
-
project {
|
|
1050
|
-
id
|
|
1051
|
-
}
|
|
1052
|
-
fieldValues(first: 20) {
|
|
1053
|
-
nodes {
|
|
1054
|
-
__typename
|
|
1055
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
1056
|
-
name
|
|
1057
|
-
optionId
|
|
1058
|
-
field {
|
|
1059
|
-
... on ProjectV2SingleSelectField {
|
|
1060
|
-
name
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
... on ProjectV2ItemFieldTextValue {
|
|
1065
|
-
text
|
|
1066
|
-
field {
|
|
1067
|
-
... on ProjectV2FieldCommon {
|
|
1068
|
-
name
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
pageInfo {
|
|
1076
|
-
endCursor
|
|
1077
|
-
hasNextPage
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
`;
|
|
1084
|
-
var ISSUE_PROJECT_ITEMS_PAGE_QUERY = `
|
|
1085
|
-
query IssueProjectItemsPage($issueId: ID!, $cursor: String) {
|
|
1086
|
-
node(id: $issueId) {
|
|
1087
|
-
__typename
|
|
1088
|
-
... on Issue {
|
|
1089
|
-
id
|
|
1090
|
-
number
|
|
1091
|
-
updatedAt
|
|
1092
|
-
repository {
|
|
1093
|
-
name
|
|
1094
|
-
url
|
|
1095
|
-
owner {
|
|
1096
|
-
login
|
|
1097
|
-
}
|
|
1098
|
-
}
|
|
1099
|
-
projectItems(first: 100, after: $cursor, includeArchived: false) {
|
|
1100
|
-
nodes {
|
|
1101
|
-
id
|
|
1102
|
-
updatedAt
|
|
1103
|
-
project {
|
|
1104
|
-
id
|
|
1105
|
-
}
|
|
1106
|
-
fieldValues(first: 20) {
|
|
1107
|
-
nodes {
|
|
1108
|
-
__typename
|
|
1109
|
-
... on ProjectV2ItemFieldSingleSelectValue {
|
|
1110
|
-
name
|
|
1111
|
-
optionId
|
|
1112
|
-
field {
|
|
1113
|
-
... on ProjectV2SingleSelectField {
|
|
1114
|
-
name
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
... on ProjectV2ItemFieldTextValue {
|
|
1119
|
-
text
|
|
1120
|
-
field {
|
|
1121
|
-
... on ProjectV2FieldCommon {
|
|
1122
|
-
name
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
}
|
|
1129
|
-
pageInfo {
|
|
1130
|
-
endCursor
|
|
1131
|
-
hasNextPage
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
`;
|
|
1138
|
-
|
|
1139
|
-
// ../tracker-github/dist/orchestrator-adapter.js
|
|
1140
|
-
import { createHash as createHash2 } from "crypto";
|
|
1141
|
-
var githubProjectTrackerAdapter = {
|
|
1142
|
-
async listIssues(project, dependencies = {}) {
|
|
1143
|
-
return listProjectIssues(project, dependencies);
|
|
1144
|
-
},
|
|
1145
|
-
async listIssuesByStates(project, states, dependencies = {}) {
|
|
1146
|
-
if (states.length === 0) {
|
|
1147
|
-
return [];
|
|
1148
|
-
}
|
|
1149
|
-
const issues = await listProjectIssues(project, dependencies);
|
|
1150
|
-
const normalizedStates = new Set(states.map((state) => state.trim().toLowerCase()));
|
|
1151
|
-
return issues.filter((issue) => normalizedStates.has(issue.state.trim().toLowerCase()));
|
|
1152
|
-
},
|
|
1153
|
-
async fetchIssueStatesByIds(project, issueIds, dependencies = {}) {
|
|
1154
|
-
if (issueIds.length === 0) {
|
|
1155
|
-
return [];
|
|
1156
|
-
}
|
|
1157
|
-
return fetchProjectIssueStatesByIds(project, issueIds, dependencies);
|
|
1158
|
-
},
|
|
1159
|
-
buildWorkerEnvironment(project) {
|
|
1160
|
-
return {
|
|
1161
|
-
GITHUB_PROJECT_ID: requireTrackerSetting(project.tracker, "projectId")
|
|
1162
|
-
};
|
|
1163
|
-
},
|
|
1164
|
-
reviveIssue(project, run) {
|
|
1165
|
-
return {
|
|
1166
|
-
id: run.issueId,
|
|
1167
|
-
identifier: run.issueIdentifier,
|
|
1168
|
-
number: parseIssueNumber(run.issueIdentifier),
|
|
1169
|
-
title: run.issueTitle ?? run.issueIdentifier,
|
|
1170
|
-
description: null,
|
|
1171
|
-
priority: null,
|
|
1172
|
-
state: run.issueState,
|
|
1173
|
-
branchName: null,
|
|
1174
|
-
url: null,
|
|
1175
|
-
labels: [],
|
|
1176
|
-
blockedBy: [],
|
|
1177
|
-
createdAt: null,
|
|
1178
|
-
updatedAt: null,
|
|
1179
|
-
repository: run.repository,
|
|
1180
|
-
tracker: {
|
|
1181
|
-
adapter: "github-project",
|
|
1182
|
-
bindingId: project.tracker.bindingId,
|
|
1183
|
-
itemId: run.issueId
|
|
1184
|
-
},
|
|
1185
|
-
metadata: {}
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
};
|
|
1189
|
-
async function listProjectIssues(project, dependencies = {}) {
|
|
1190
|
-
const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
|
|
1191
|
-
const loadProjectIssues = () => fetchGithubProjectIssues(trackerConfig, dependencies.fetchImpl);
|
|
1192
|
-
return dependencies.projectItemsCache?.getOrLoad(buildProjectItemsCacheKey(trackerConfig, dependencies), loadProjectIssues) ?? loadProjectIssues();
|
|
1193
|
-
}
|
|
1194
|
-
async function fetchProjectIssueStatesByIds(project, issueIds, dependencies = {}) {
|
|
1195
|
-
const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
|
|
1196
|
-
return fetchGithubIssueStatesByIds(trackerConfig, [...issueIds], dependencies.fetchImpl);
|
|
1197
|
-
}
|
|
1198
|
-
function resolveGitHubTrackerConfig(project, dependencies = {}) {
|
|
1199
|
-
const token = dependencies.token ?? process.env.GITHUB_GRAPHQL_TOKEN;
|
|
1200
|
-
if (!token) {
|
|
1201
|
-
throw new Error("GITHUB_GRAPHQL_TOKEN environment variable is required. Run 'gh auth token' or set the variable.");
|
|
1202
|
-
}
|
|
1203
|
-
const githubProjectId = requireTrackerSetting(project.tracker, "projectId");
|
|
1204
|
-
return {
|
|
1205
|
-
projectId: githubProjectId,
|
|
1206
|
-
token,
|
|
1207
|
-
apiUrl: project.tracker.apiUrl,
|
|
1208
|
-
assignedOnly: readBooleanTrackerSetting(project.tracker, "assignedOnly"),
|
|
1209
|
-
priorityFieldName: readOptionalStringTrackerSetting(project.tracker, "priorityFieldName"),
|
|
1210
|
-
timeoutMs: readNumberTrackerSetting(project.tracker, "timeoutMs")
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
function buildProjectItemsCacheKey(config, _dependencies) {
|
|
1214
|
-
return JSON.stringify({
|
|
1215
|
-
adapter: "github-project",
|
|
1216
|
-
apiUrl: config.apiUrl,
|
|
1217
|
-
assignedOnly: config.assignedOnly ?? false,
|
|
1218
|
-
priorityFieldName: config.priorityFieldName ?? null,
|
|
1219
|
-
projectId: config.projectId,
|
|
1220
|
-
timeoutMs: config.timeoutMs,
|
|
1221
|
-
tokenFingerprint: hashToken(config.token)
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
function hashToken(token) {
|
|
1225
|
-
if (!token) {
|
|
1226
|
-
return null;
|
|
1227
|
-
}
|
|
1228
|
-
return createHash2("sha256").update(token).digest("hex");
|
|
1229
|
-
}
|
|
1230
|
-
var trackerAdapters = {
|
|
1231
|
-
"github-project": githubProjectTrackerAdapter
|
|
1232
|
-
};
|
|
1233
|
-
function resolveTrackerAdapter(tracker) {
|
|
1234
|
-
const adapter = trackerAdapters[tracker.adapter];
|
|
1235
|
-
if (!adapter) {
|
|
1236
|
-
throw new Error(`Unsupported tracker adapter: ${tracker.adapter}`);
|
|
1237
|
-
}
|
|
1238
|
-
return adapter;
|
|
1239
|
-
}
|
|
1240
|
-
function requireTrackerSetting(tracker, key) {
|
|
1241
|
-
const value = tracker.settings?.[key];
|
|
1242
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
1243
|
-
throw new Error(`Tracker adapter "${tracker.adapter}" requires the "${key}" setting.`);
|
|
1244
|
-
}
|
|
1245
|
-
return value;
|
|
1246
|
-
}
|
|
1247
|
-
function readBooleanTrackerSetting(tracker, key) {
|
|
1248
|
-
const value = tracker.settings?.[key];
|
|
1249
|
-
return value === true || value === "true";
|
|
1250
|
-
}
|
|
1251
|
-
function readNumberTrackerSetting(tracker, key) {
|
|
1252
|
-
const value = tracker.settings?.[key];
|
|
1253
|
-
if (value === void 0) {
|
|
1254
|
-
return void 0;
|
|
1255
|
-
}
|
|
1256
|
-
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
1257
|
-
return value;
|
|
1258
|
-
}
|
|
1259
|
-
if (typeof value === "string") {
|
|
1260
|
-
const parsed = Number(value);
|
|
1261
|
-
if (Number.isInteger(parsed) && parsed > 0) {
|
|
1262
|
-
return parsed;
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
throw new Error(`Tracker adapter "${tracker.adapter}" requires the "${key}" setting to be a positive integer when provided.`);
|
|
1266
|
-
}
|
|
1267
|
-
function readOptionalStringTrackerSetting(tracker, key) {
|
|
1268
|
-
const value = tracker.settings?.[key];
|
|
1269
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1270
|
-
}
|
|
1271
|
-
function parseIssueNumber(identifier) {
|
|
1272
|
-
const match = identifier.match(/#(\d+)$/);
|
|
1273
|
-
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// ../tracker-file/dist/file-tracker-adapter.js
|
|
1277
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
1278
|
-
function requireTrackerSetting2(project, key) {
|
|
1279
|
-
const value = project.tracker.settings?.[key];
|
|
1280
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
1281
|
-
throw new Error(`Tracker adapter "file" requires the "${key}" setting.`);
|
|
1282
|
-
}
|
|
1283
|
-
return value;
|
|
1284
|
-
}
|
|
1285
|
-
function parseIssueNumber2(identifier) {
|
|
1286
|
-
const match = identifier.match(/#(\d+)$/);
|
|
1287
|
-
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
1288
|
-
}
|
|
1289
|
-
function isValidIssueShape(entry) {
|
|
1290
|
-
if (!entry || typeof entry !== "object")
|
|
1291
|
-
return false;
|
|
1292
|
-
const e = entry;
|
|
1293
|
-
return typeof e.id === "string" && typeof e.identifier === "string" && typeof e.state === "string" && e.repository !== null && typeof e.repository === "object" && e.tracker !== null && typeof e.tracker === "object";
|
|
1294
|
-
}
|
|
1295
|
-
var fileTrackerAdapter = {
|
|
1296
|
-
async listIssues(project) {
|
|
1297
|
-
const issuesPath = requireTrackerSetting2(project, "issuesPath");
|
|
1298
|
-
try {
|
|
1299
|
-
const raw = await readFile2(issuesPath, "utf-8");
|
|
1300
|
-
const parsed = JSON.parse(raw);
|
|
1301
|
-
if (!Array.isArray(parsed)) {
|
|
1302
|
-
throw new Error(`Expected an array of issues in ${issuesPath}, got ${typeof parsed}`);
|
|
1303
|
-
}
|
|
1304
|
-
const valid = [];
|
|
1305
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
1306
|
-
if (isValidIssueShape(parsed[i])) {
|
|
1307
|
-
valid.push(parsed[i]);
|
|
1308
|
-
} else {
|
|
1309
|
-
process.stderr.write(`[tracker-file] Skipping invalid issue at index ${i} in ${issuesPath}
|
|
1310
|
-
`);
|
|
1004
|
+
allowPartialFirstLine: position > 0
|
|
1005
|
+
});
|
|
1006
|
+
if (events.length >= limit) {
|
|
1007
|
+
return events;
|
|
1008
|
+
}
|
|
1311
1009
|
}
|
|
1010
|
+
return parseRecentEvents(tail.toString("utf8"), limit, {
|
|
1011
|
+
allowPartialFirstLine: false
|
|
1012
|
+
});
|
|
1013
|
+
} finally {
|
|
1014
|
+
await handle.close();
|
|
1312
1015
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
1316
|
-
return [];
|
|
1317
|
-
}
|
|
1318
|
-
if (err instanceof SyntaxError) {
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
if (isFileMissing(error)) {
|
|
1319
1018
|
return [];
|
|
1320
1019
|
}
|
|
1321
|
-
throw
|
|
1020
|
+
throw error;
|
|
1322
1021
|
}
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1022
|
+
}
|
|
1023
|
+
issueWorkspaceDir(projectId, workspaceKey) {
|
|
1024
|
+
return join2(this.runtimeRoot, workspaceKey);
|
|
1025
|
+
}
|
|
1026
|
+
async loadIssueWorkspace(projectId, workspaceKey) {
|
|
1027
|
+
return await readJsonFile(
|
|
1028
|
+
join2(this.issueWorkspaceDir(projectId, workspaceKey), "workspace.json")
|
|
1029
|
+
) ?? null;
|
|
1030
|
+
}
|
|
1031
|
+
async loadIssueWorkspaces(projectId) {
|
|
1032
|
+
const entries = await safeReadDir(this.runtimeRoot);
|
|
1033
|
+
const records = await Promise.all(
|
|
1034
|
+
entries.map(async (entry) => {
|
|
1035
|
+
if (!await this.isIssueWorkspaceEntry(entry)) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
return this.loadIssueWorkspace(projectId, entry);
|
|
1039
|
+
})
|
|
1040
|
+
);
|
|
1041
|
+
return records.filter(
|
|
1042
|
+
(record) => Boolean(record)
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
async isIssueWorkspaceEntry(entry) {
|
|
1046
|
+
if (entry.startsWith(".") || entry === "cache" || entry === "issues.json" || entry === "project.json" || entry === "runs" || entry === "status.json") {
|
|
1047
|
+
return false;
|
|
1327
1048
|
}
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
async fetchIssueStatesByIds(project, issueIds) {
|
|
1333
|
-
if (issueIds.length === 0) {
|
|
1334
|
-
return [];
|
|
1049
|
+
try {
|
|
1050
|
+
return (await stat2(join2(this.runtimeRoot, entry))).isDirectory();
|
|
1051
|
+
} catch {
|
|
1052
|
+
return false;
|
|
1335
1053
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
}
|
|
1054
|
+
}
|
|
1055
|
+
async saveIssueWorkspace(record) {
|
|
1056
|
+
await writeJsonFile(
|
|
1057
|
+
join2(
|
|
1058
|
+
this.issueWorkspaceDir(record.projectId, record.workspaceKey),
|
|
1059
|
+
"workspace.json"
|
|
1060
|
+
),
|
|
1061
|
+
record
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
async removeIssueWorkspace(projectId, workspaceKey) {
|
|
1065
|
+
const dir = this.issueWorkspaceDir(projectId, workspaceKey);
|
|
1066
|
+
await rm2(dir, { recursive: true, force: true });
|
|
1067
|
+
}
|
|
1068
|
+
async findRunDir(runId) {
|
|
1069
|
+
const candidate = this.runDir(runId);
|
|
1070
|
+
const run = await readJsonFile(
|
|
1071
|
+
join2(candidate, "run.json")
|
|
1072
|
+
);
|
|
1073
|
+
if (run || await pathExists2(join2(candidate, "events.ndjson"))) {
|
|
1074
|
+
return candidate;
|
|
1075
|
+
}
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
resolveMirroredEventsPath(primaryPath) {
|
|
1079
|
+
if (!this.resolvedEventsMirrorRoot) {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
const relativePath = relative(this.resolvedRuntimeRoot, primaryPath);
|
|
1083
|
+
if (relativePath.startsWith("..")) {
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
const mirrorPath = join2(this.resolvedEventsMirrorRoot, relativePath);
|
|
1087
|
+
return mirrorPath === primaryPath ? null : mirrorPath;
|
|
1368
1088
|
}
|
|
1369
1089
|
};
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1090
|
+
async function writeJsonFile(path, value) {
|
|
1091
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
1092
|
+
const temporaryPath = `${path}.tmp`;
|
|
1093
|
+
await writeFile2(temporaryPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
1094
|
+
await rename2(temporaryPath, path);
|
|
1095
|
+
}
|
|
1096
|
+
async function pathExists2(path) {
|
|
1097
|
+
try {
|
|
1098
|
+
await stat2(path);
|
|
1099
|
+
return true;
|
|
1100
|
+
} catch (error) {
|
|
1101
|
+
if (isFileMissing(error)) {
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
throw error;
|
|
1105
|
+
}
|
|
1380
1106
|
}
|
|
1381
1107
|
|
|
1382
|
-
// ../orchestrator/
|
|
1108
|
+
// ../orchestrator/src/service.ts
|
|
1383
1109
|
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
1384
1110
|
var DEFAULT_CONCURRENCY = 3;
|
|
1385
1111
|
var DEFAULT_RETRY_BACKOFF_MS = 3e4;
|
|
@@ -1387,7 +1113,7 @@ var CONTINUATION_RETRY_DELAY_MS = 1e3;
|
|
|
1387
1113
|
var DEFAULT_WORKER_COMMAND = "node packages/worker/dist/index.js";
|
|
1388
1114
|
var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
|
|
1389
1115
|
var LOW_RATE_LIMIT_WARNING_THRESHOLD = 0.05;
|
|
1390
|
-
var
|
|
1116
|
+
var MAX_FAILURE_RETRIES_EXCEEDED_REASON2 = "max_failure_retries_exceeded";
|
|
1391
1117
|
var STUCK_WORKER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
1392
1118
|
function isUsableWorkflowResolution(resolution) {
|
|
1393
1119
|
return resolution.isValid || resolution.usedLastKnownGood;
|
|
@@ -1410,9 +1136,11 @@ function parseFiniteNumber(value) {
|
|
|
1410
1136
|
return null;
|
|
1411
1137
|
}
|
|
1412
1138
|
var OrchestratorService = class {
|
|
1413
|
-
store
|
|
1414
|
-
|
|
1415
|
-
|
|
1139
|
+
constructor(store, projectConfig, dependencies = {}) {
|
|
1140
|
+
this.store = store;
|
|
1141
|
+
this.projectConfig = projectConfig;
|
|
1142
|
+
this.dependencies = dependencies;
|
|
1143
|
+
}
|
|
1416
1144
|
projectPollIntervals = /* @__PURE__ */ new Map();
|
|
1417
1145
|
activeWorkerPids = /* @__PURE__ */ new Set();
|
|
1418
1146
|
workerStderrBuffers = /* @__PURE__ */ new Map();
|
|
@@ -1427,23 +1155,25 @@ var OrchestratorService = class {
|
|
|
1427
1155
|
sleepResolver = null;
|
|
1428
1156
|
reconcilePromise = Promise.resolve();
|
|
1429
1157
|
reconcileRequested = false;
|
|
1430
|
-
constructor(store, projectConfig, dependencies = {}) {
|
|
1431
|
-
this.store = store;
|
|
1432
|
-
this.projectConfig = projectConfig;
|
|
1433
|
-
this.dependencies = dependencies;
|
|
1434
|
-
}
|
|
1435
1158
|
async run(options = {}) {
|
|
1436
1159
|
this.running = true;
|
|
1437
|
-
await this.runSerialized(
|
|
1160
|
+
await this.runSerialized(
|
|
1161
|
+
() => this.performStartupCleanup(this.createTrackerDependencies())
|
|
1162
|
+
);
|
|
1438
1163
|
while (this.running) {
|
|
1439
1164
|
try {
|
|
1440
|
-
const snapshot = await this.runOnceInternal(
|
|
1165
|
+
const snapshot = await this.runOnceInternal(
|
|
1166
|
+
options.issueIdentifier,
|
|
1167
|
+
this.createTrackerDependencies()
|
|
1168
|
+
);
|
|
1441
1169
|
await this.notifyTick(snapshot);
|
|
1442
1170
|
} catch (error) {
|
|
1443
1171
|
if (options.once) {
|
|
1444
1172
|
throw error;
|
|
1445
1173
|
}
|
|
1446
|
-
this.writeStderr(
|
|
1174
|
+
this.writeStderr(
|
|
1175
|
+
`[orchestrator] run loop failed for ${this.projectConfig.projectId}: ${this.formatErrorMessage(error)}`
|
|
1176
|
+
);
|
|
1447
1177
|
}
|
|
1448
1178
|
if (options.once || !this.running) {
|
|
1449
1179
|
return;
|
|
@@ -1452,20 +1182,38 @@ var OrchestratorService = class {
|
|
|
1452
1182
|
}
|
|
1453
1183
|
}
|
|
1454
1184
|
async runOnce(options = {}) {
|
|
1455
|
-
return this.runOnceInternal(
|
|
1185
|
+
return this.runOnceInternal(
|
|
1186
|
+
options.issueIdentifier,
|
|
1187
|
+
this.createTrackerDependencies()
|
|
1188
|
+
);
|
|
1456
1189
|
}
|
|
1457
1190
|
async status() {
|
|
1458
1191
|
return this.store.loadProjectStatus(this.projectConfig.projectId);
|
|
1459
1192
|
}
|
|
1460
1193
|
async statusForIssue(issueIdentifier) {
|
|
1461
|
-
const issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
1462
|
-
|
|
1194
|
+
const issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
1195
|
+
this.projectConfig.projectId
|
|
1196
|
+
);
|
|
1197
|
+
const issueRecord = issueRecords.find(
|
|
1198
|
+
(record) => record.identifier === issueIdentifier
|
|
1199
|
+
);
|
|
1463
1200
|
if (!issueRecord) {
|
|
1464
1201
|
return null;
|
|
1465
1202
|
}
|
|
1466
|
-
const currentRunCandidate = issueRecord.currentRunId ? await this.store.loadRun(
|
|
1467
|
-
|
|
1468
|
-
|
|
1203
|
+
const currentRunCandidate = issueRecord.currentRunId ? await this.store.loadRun(
|
|
1204
|
+
issueRecord.currentRunId,
|
|
1205
|
+
this.projectConfig.projectId
|
|
1206
|
+
) : null;
|
|
1207
|
+
const currentRun = isMatchingIssueRun(
|
|
1208
|
+
currentRunCandidate,
|
|
1209
|
+
issueRecord.issueId,
|
|
1210
|
+
issueIdentifier
|
|
1211
|
+
) ? currentRunCandidate : await this.findLatestRunForIssue(issueRecord.issueId, issueIdentifier);
|
|
1212
|
+
const recentEvents = currentRun === null ? [] : await this.store.loadRecentRunEvents(
|
|
1213
|
+
currentRun.runId,
|
|
1214
|
+
20,
|
|
1215
|
+
currentRun.projectId
|
|
1216
|
+
);
|
|
1469
1217
|
const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
|
|
1470
1218
|
const currentAttempt = currentRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
|
|
1471
1219
|
return {
|
|
@@ -1502,7 +1250,10 @@ var OrchestratorService = class {
|
|
|
1502
1250
|
codex_session_logs: currentRun === null ? [] : [
|
|
1503
1251
|
{
|
|
1504
1252
|
label: "worker",
|
|
1505
|
-
path: join3(
|
|
1253
|
+
path: join3(
|
|
1254
|
+
this.store.runDir(currentRun.runId, currentRun.projectId),
|
|
1255
|
+
"worker.log"
|
|
1256
|
+
),
|
|
1506
1257
|
url: null
|
|
1507
1258
|
}
|
|
1508
1259
|
]
|
|
@@ -1564,7 +1315,9 @@ var OrchestratorService = class {
|
|
|
1564
1315
|
if (this.dependencies.pollIntervalMs) {
|
|
1565
1316
|
return this.dependencies.pollIntervalMs;
|
|
1566
1317
|
}
|
|
1567
|
-
const configuredIntervals = [...this.projectPollIntervals.values()].filter(
|
|
1318
|
+
const configuredIntervals = [...this.projectPollIntervals.values()].filter(
|
|
1319
|
+
(value) => Number.isFinite(value) && value > 0
|
|
1320
|
+
);
|
|
1568
1321
|
return configuredIntervals.length ? Math.min(...configuredIntervals) : DEFAULT_POLL_INTERVAL_MS;
|
|
1569
1322
|
}
|
|
1570
1323
|
async reconcileProject(tenant, issueIdentifier, trackerDependencies = {}) {
|
|
@@ -1577,27 +1330,57 @@ var OrchestratorService = class {
|
|
|
1577
1330
|
let pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
1578
1331
|
let rateLimits = null;
|
|
1579
1332
|
let trackerRateLimits = null;
|
|
1580
|
-
let issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
1581
|
-
|
|
1582
|
-
|
|
1333
|
+
let issueRecords = await this.store.loadProjectIssueOrchestrations(
|
|
1334
|
+
tenant.projectId
|
|
1335
|
+
);
|
|
1336
|
+
const allRuns = (await this.store.loadAllRuns()).filter(
|
|
1337
|
+
(run) => run.projectId === tenant.projectId
|
|
1338
|
+
);
|
|
1339
|
+
const activeRuns = allRuns.filter((run) => isActiveRunRecordStatus(run.status));
|
|
1583
1340
|
for (const run of activeRuns) {
|
|
1584
|
-
const outcome = await this.reconcileRun(
|
|
1341
|
+
const outcome = await this.reconcileRun(
|
|
1342
|
+
tenant,
|
|
1343
|
+
run,
|
|
1344
|
+
issueRecords,
|
|
1345
|
+
trackerDependencies
|
|
1346
|
+
);
|
|
1585
1347
|
issueRecords = outcome.issueRecords;
|
|
1586
1348
|
if (outcome.recovered) {
|
|
1587
1349
|
recovered += 1;
|
|
1588
1350
|
}
|
|
1589
1351
|
}
|
|
1590
|
-
const reconciledRuns = (await this.store.loadAllRuns()).filter(
|
|
1591
|
-
|
|
1352
|
+
const reconciledRuns = (await this.store.loadAllRuns()).filter(
|
|
1353
|
+
(run) => run.projectId === tenant.projectId && isActiveRunRecordStatus(run.status)
|
|
1354
|
+
);
|
|
1355
|
+
const projectRunsAfterReconcile = (await this.store.loadAllRuns()).filter(
|
|
1356
|
+
(run) => run.projectId === tenant.projectId
|
|
1357
|
+
);
|
|
1592
1358
|
rateLimits = resolveProjectRateLimits(reconciledRuns, []);
|
|
1593
1359
|
try {
|
|
1594
1360
|
pollIntervalMs = await this.loadProjectPollInterval(tenant);
|
|
1595
|
-
const currentActiveRuns = (await this.store.loadAllRuns()).filter(
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
const
|
|
1361
|
+
const currentActiveRuns = (await this.store.loadAllRuns()).filter(
|
|
1362
|
+
(run) => run.projectId === tenant.projectId && isActiveRunRecordStatus(run.status)
|
|
1363
|
+
);
|
|
1364
|
+
const {
|
|
1365
|
+
runs: syncedActiveRuns,
|
|
1366
|
+
issuesByIdentifier: syncedIssuesByIdentifier
|
|
1367
|
+
} = await this.syncActiveRunIssueStates(
|
|
1368
|
+
tenant,
|
|
1369
|
+
trackerAdapter,
|
|
1370
|
+
currentActiveRuns,
|
|
1371
|
+
now
|
|
1372
|
+
);
|
|
1373
|
+
const issues = await trackerAdapter.listIssues(
|
|
1374
|
+
tenant,
|
|
1375
|
+
trackerDependencies
|
|
1376
|
+
);
|
|
1377
|
+
const filteredIssues = issueIdentifier ? issues.filter(
|
|
1378
|
+
(issue) => issue.identifier === issueIdentifier
|
|
1379
|
+
) : issues;
|
|
1599
1380
|
const { candidates: actionableCandidates, lifecycle } = await this.resolveActionableCandidates(tenant, filteredIssues);
|
|
1600
|
-
const trackedIssuesByIdentifier = new Map(
|
|
1381
|
+
const trackedIssuesByIdentifier = new Map(
|
|
1382
|
+
syncedIssuesByIdentifier
|
|
1383
|
+
);
|
|
1601
1384
|
for (const issue of filteredIssues) {
|
|
1602
1385
|
const existing = trackedIssuesByIdentifier.get(issue.identifier);
|
|
1603
1386
|
trackedIssuesByIdentifier.set(issue.identifier, {
|
|
@@ -1618,17 +1401,33 @@ var OrchestratorService = class {
|
|
|
1618
1401
|
rateLimits: existing.rateLimits ?? issue.rateLimits ?? null
|
|
1619
1402
|
});
|
|
1620
1403
|
}
|
|
1621
|
-
rateLimits = resolveProjectRateLimits(
|
|
1622
|
-
|
|
1404
|
+
rateLimits = resolveProjectRateLimits(
|
|
1405
|
+
syncedActiveRuns,
|
|
1406
|
+
trackedIssuesByIdentifier.values()
|
|
1407
|
+
);
|
|
1408
|
+
trackerRateLimits = resolveTrackerRateLimits(
|
|
1409
|
+
trackedIssuesByIdentifier.values()
|
|
1410
|
+
);
|
|
1623
1411
|
const concurrency = await this.getProjectConcurrency(tenant);
|
|
1624
|
-
const currentlyActive = issueRecords.filter(
|
|
1412
|
+
const currentlyActive = issueRecords.filter(
|
|
1413
|
+
(record) => isIssueOrchestrationClaimedState(record.state)
|
|
1414
|
+
).length;
|
|
1625
1415
|
const availableSlots = Math.max(0, concurrency - currentlyActive);
|
|
1626
|
-
const latestRunsByIssueId = buildLatestRunMapByIssueId(
|
|
1416
|
+
const latestRunsByIssueId = buildLatestRunMapByIssueId(
|
|
1417
|
+
projectRunsAfterReconcile
|
|
1418
|
+
);
|
|
1627
1419
|
const unscheduledCandidates = actionableCandidates.filter((issue) => {
|
|
1628
|
-
if (
|
|
1420
|
+
if (hasConvergenceLockedRunForIssue(
|
|
1421
|
+
projectRunsAfterReconcile,
|
|
1422
|
+
issue.id,
|
|
1423
|
+
issue.state,
|
|
1424
|
+
issue.updatedAt
|
|
1425
|
+
)) {
|
|
1629
1426
|
return false;
|
|
1630
1427
|
}
|
|
1631
|
-
return !issueRecords.some(
|
|
1428
|
+
return !issueRecords.some(
|
|
1429
|
+
(record) => record.issueId === issue.id && isIssueOrchestrationClaimedState(record.state)
|
|
1430
|
+
);
|
|
1632
1431
|
});
|
|
1633
1432
|
const sortedCandidates = sortCandidatesForDispatch(unscheduledCandidates);
|
|
1634
1433
|
const activeByState = /* @__PURE__ */ new Map();
|
|
@@ -1643,9 +1442,13 @@ var OrchestratorService = class {
|
|
|
1643
1442
|
if (this.shuttingDown) {
|
|
1644
1443
|
break;
|
|
1645
1444
|
}
|
|
1646
|
-
if (slotsRemaining <= 0)
|
|
1647
|
-
|
|
1648
|
-
|
|
1445
|
+
if (slotsRemaining <= 0) break;
|
|
1446
|
+
if (await this.isFailureRetrySuppressedIssue(
|
|
1447
|
+
tenant,
|
|
1448
|
+
issue,
|
|
1449
|
+
issueRecords,
|
|
1450
|
+
latestRunsByIssueId.get(issue.id) ?? null
|
|
1451
|
+
)) {
|
|
1649
1452
|
continue;
|
|
1650
1453
|
}
|
|
1651
1454
|
const stateLimit = maxConcurrentByState[issue.state];
|
|
@@ -1655,11 +1458,13 @@ var OrchestratorService = class {
|
|
|
1655
1458
|
continue;
|
|
1656
1459
|
}
|
|
1657
1460
|
}
|
|
1658
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1461
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1462
|
+
{
|
|
1463
|
+
adapter: issue.tracker.adapter,
|
|
1464
|
+
issueSubjectId: issue.id
|
|
1465
|
+
},
|
|
1466
|
+
issue.identifier
|
|
1467
|
+
);
|
|
1663
1468
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
1664
1469
|
issueId: issue.id,
|
|
1665
1470
|
identifier: issue.identifier,
|
|
@@ -1695,13 +1500,18 @@ var OrchestratorService = class {
|
|
|
1695
1500
|
issueId: run.issueId,
|
|
1696
1501
|
issueState: issue.state
|
|
1697
1502
|
});
|
|
1698
|
-
this.logVerbose(
|
|
1503
|
+
this.logVerbose(
|
|
1504
|
+
`[dispatch] Issue ${issue.identifier} \u2192 run ${run.runId}`
|
|
1505
|
+
);
|
|
1699
1506
|
dispatched += 1;
|
|
1700
1507
|
slotsRemaining -= 1;
|
|
1701
|
-
activeByState.set(
|
|
1508
|
+
activeByState.set(
|
|
1509
|
+
issue.state,
|
|
1510
|
+
(activeByState.get(issue.state) ?? 0) + 1
|
|
1511
|
+
);
|
|
1702
1512
|
}
|
|
1703
1513
|
for (const issueRecord of issueRecords) {
|
|
1704
|
-
if (!
|
|
1514
|
+
if (!isIssueOrchestrationClaimedState(issueRecord.state)) {
|
|
1705
1515
|
continue;
|
|
1706
1516
|
}
|
|
1707
1517
|
const issue = trackedIssuesByIdentifier.get(issueRecord.identifier);
|
|
@@ -1709,8 +1519,16 @@ var OrchestratorService = class {
|
|
|
1709
1519
|
continue;
|
|
1710
1520
|
}
|
|
1711
1521
|
const persistedRun = issueRecord.currentRunId ? await this.store.loadRun(issueRecord.currentRunId, tenant.projectId) : null;
|
|
1712
|
-
const activeRun = syncedActiveRuns.find(
|
|
1713
|
-
|
|
1522
|
+
const activeRun = syncedActiveRuns.find(
|
|
1523
|
+
(run) => isMatchingIssueRun(
|
|
1524
|
+
run,
|
|
1525
|
+
issueRecord.issueId,
|
|
1526
|
+
issueRecord.identifier
|
|
1527
|
+
)
|
|
1528
|
+
) ?? persistedRun;
|
|
1529
|
+
const resolvedIssue = actionableCandidates.find(
|
|
1530
|
+
(candidate) => candidate.identifier === issue.identifier
|
|
1531
|
+
);
|
|
1714
1532
|
if (resolvedIssue) {
|
|
1715
1533
|
continue;
|
|
1716
1534
|
}
|
|
@@ -1729,9 +1547,15 @@ var OrchestratorService = class {
|
|
|
1729
1547
|
lastError: "Run suppressed because the tracker state is no longer actionable."
|
|
1730
1548
|
};
|
|
1731
1549
|
await this.store.saveRun(suppressedRun);
|
|
1732
|
-
this.logVerbose(
|
|
1550
|
+
this.logVerbose(
|
|
1551
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
1552
|
+
);
|
|
1733
1553
|
}
|
|
1734
|
-
issueRecords = releaseIssueOrchestration(
|
|
1554
|
+
issueRecords = releaseIssueOrchestration(
|
|
1555
|
+
issueRecords,
|
|
1556
|
+
issueRecord.issueId,
|
|
1557
|
+
now
|
|
1558
|
+
);
|
|
1735
1559
|
suppressed += 1;
|
|
1736
1560
|
}
|
|
1737
1561
|
const terminalIssuesByIdentifier = /* @__PURE__ */ new Map();
|
|
@@ -1747,14 +1571,28 @@ var OrchestratorService = class {
|
|
|
1747
1571
|
} catch (error) {
|
|
1748
1572
|
lastError = error instanceof Error ? error.message : "Unknown orchestration error";
|
|
1749
1573
|
}
|
|
1750
|
-
const effectivePollIntervalMs = resolveAdaptivePollIntervalMs(
|
|
1574
|
+
const effectivePollIntervalMs = resolveAdaptivePollIntervalMs(
|
|
1575
|
+
pollIntervalMs,
|
|
1576
|
+
trackerRateLimits
|
|
1577
|
+
);
|
|
1751
1578
|
if (effectivePollIntervalMs > pollIntervalMs && isLowRateLimit(trackerRateLimits, LOW_RATE_LIMIT_WARNING_THRESHOLD)) {
|
|
1752
|
-
this.writeStderr(
|
|
1579
|
+
this.writeStderr(
|
|
1580
|
+
`[orchestrator] low GitHub rate limit for ${tenant.projectId}: interval=${effectivePollIntervalMs}ms rateLimits=${JSON.stringify(
|
|
1581
|
+
trackerRateLimits
|
|
1582
|
+
)}`
|
|
1583
|
+
);
|
|
1753
1584
|
}
|
|
1754
1585
|
this.projectPollIntervals.set(tenant.projectId, effectivePollIntervalMs);
|
|
1755
|
-
await this.store.saveProjectIssueOrchestrations(
|
|
1756
|
-
|
|
1757
|
-
|
|
1586
|
+
await this.store.saveProjectIssueOrchestrations(
|
|
1587
|
+
tenant.projectId,
|
|
1588
|
+
issueRecords
|
|
1589
|
+
);
|
|
1590
|
+
const allTenantRuns = (await this.store.loadAllRuns()).filter(
|
|
1591
|
+
(run) => run.projectId === tenant.projectId
|
|
1592
|
+
);
|
|
1593
|
+
const latestRuns = allTenantRuns.filter(
|
|
1594
|
+
(run) => isActiveRunRecordStatus(run.status)
|
|
1595
|
+
);
|
|
1758
1596
|
rateLimits = rateLimits ?? resolveProjectRateLimits(latestRuns, []);
|
|
1759
1597
|
const status = buildProjectSnapshot({
|
|
1760
1598
|
project: tenant,
|
|
@@ -1771,7 +1609,9 @@ var OrchestratorService = class {
|
|
|
1771
1609
|
async performStartupCleanup(trackerDependencies = {}) {
|
|
1772
1610
|
const tenant = this.projectConfig;
|
|
1773
1611
|
const now = this.now();
|
|
1774
|
-
const workspaceRecords = await this.store.loadIssueWorkspaces(
|
|
1612
|
+
const workspaceRecords = await this.store.loadIssueWorkspaces(
|
|
1613
|
+
tenant.projectId
|
|
1614
|
+
);
|
|
1775
1615
|
if (workspaceRecords.length === 0) {
|
|
1776
1616
|
return;
|
|
1777
1617
|
}
|
|
@@ -1779,10 +1619,20 @@ var OrchestratorService = class {
|
|
|
1779
1619
|
const workflowCache = /* @__PURE__ */ new Map();
|
|
1780
1620
|
let issues;
|
|
1781
1621
|
try {
|
|
1782
|
-
issues = await trackerAdapter.listIssuesByStates(
|
|
1622
|
+
issues = await trackerAdapter.listIssuesByStates(
|
|
1623
|
+
tenant,
|
|
1624
|
+
await this.resolveStartupCleanupTerminalStates(
|
|
1625
|
+
tenant,
|
|
1626
|
+
workspaceRecords,
|
|
1627
|
+
workflowCache
|
|
1628
|
+
),
|
|
1629
|
+
trackerDependencies
|
|
1630
|
+
);
|
|
1783
1631
|
} catch (error) {
|
|
1784
1632
|
const message = error instanceof Error ? error.message : "Unknown tracker error";
|
|
1785
|
-
console.warn(
|
|
1633
|
+
console.warn(
|
|
1634
|
+
`[orchestrator] Startup cleanup skipped for project ${tenant.projectId}: ${message}`
|
|
1635
|
+
);
|
|
1786
1636
|
return;
|
|
1787
1637
|
}
|
|
1788
1638
|
const issuesById = new Map(issues.map((issue) => [issue.id, issue]));
|
|
@@ -1795,17 +1645,27 @@ var OrchestratorService = class {
|
|
|
1795
1645
|
continue;
|
|
1796
1646
|
}
|
|
1797
1647
|
try {
|
|
1798
|
-
const resolution = await this.loadStartupCleanupWorkflow(
|
|
1648
|
+
const resolution = await this.loadStartupCleanupWorkflow(
|
|
1649
|
+
tenant,
|
|
1650
|
+
workflowCache
|
|
1651
|
+
);
|
|
1799
1652
|
if (!resolution.isValid) {
|
|
1800
1653
|
continue;
|
|
1801
1654
|
}
|
|
1802
1655
|
if (!isStateTerminal(issue.state, resolution.lifecycle)) {
|
|
1803
1656
|
continue;
|
|
1804
1657
|
}
|
|
1805
|
-
await this.cleanupTerminalIssueWorkspace(
|
|
1658
|
+
await this.cleanupTerminalIssueWorkspace(
|
|
1659
|
+
tenant,
|
|
1660
|
+
issue,
|
|
1661
|
+
now,
|
|
1662
|
+
resolution
|
|
1663
|
+
);
|
|
1806
1664
|
} catch (error) {
|
|
1807
1665
|
const message = error instanceof Error ? error.message : "Unknown startup cleanup error";
|
|
1808
|
-
console.warn(
|
|
1666
|
+
console.warn(
|
|
1667
|
+
`[orchestrator] Startup cleanup skipped workspace for ${issue.identifier}: ${message}`
|
|
1668
|
+
);
|
|
1809
1669
|
}
|
|
1810
1670
|
}
|
|
1811
1671
|
}
|
|
@@ -1816,7 +1676,9 @@ var OrchestratorService = class {
|
|
|
1816
1676
|
try {
|
|
1817
1677
|
await this.dependencies.onTick(snapshot);
|
|
1818
1678
|
} catch (error) {
|
|
1819
|
-
this.writeStderr(
|
|
1679
|
+
this.writeStderr(
|
|
1680
|
+
`[orchestrator] onTick callback failed: ${this.formatErrorMessage(error)}`
|
|
1681
|
+
);
|
|
1820
1682
|
}
|
|
1821
1683
|
}
|
|
1822
1684
|
formatErrorMessage(error) {
|
|
@@ -1825,25 +1687,22 @@ var OrchestratorService = class {
|
|
|
1825
1687
|
}
|
|
1826
1688
|
return String(error);
|
|
1827
1689
|
}
|
|
1828
|
-
async resolveStartupCleanupTerminalStates(tenant,
|
|
1690
|
+
async resolveStartupCleanupTerminalStates(tenant, _workspaceRecords, workflowCache) {
|
|
1829
1691
|
const terminalStates = /* @__PURE__ */ new Map();
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
for (const state of resolution.lifecycle.terminalStates) {
|
|
1842
|
-
const normalizedState = state.trim().toLowerCase();
|
|
1843
|
-
if (!terminalStates.has(normalizedState)) {
|
|
1844
|
-
terminalStates.set(normalizedState, state);
|
|
1692
|
+
try {
|
|
1693
|
+
const resolution = await this.loadStartupCleanupWorkflow(
|
|
1694
|
+
tenant,
|
|
1695
|
+
workflowCache
|
|
1696
|
+
);
|
|
1697
|
+
if (isUsableWorkflowResolution(resolution)) {
|
|
1698
|
+
for (const state of resolution.lifecycle.terminalStates) {
|
|
1699
|
+
const normalizedState = state.trim().toLowerCase();
|
|
1700
|
+
if (!terminalStates.has(normalizedState)) {
|
|
1701
|
+
terminalStates.set(normalizedState, state);
|
|
1702
|
+
}
|
|
1845
1703
|
}
|
|
1846
1704
|
}
|
|
1705
|
+
} catch {
|
|
1847
1706
|
}
|
|
1848
1707
|
if (terminalStates.size === 0) {
|
|
1849
1708
|
for (const state of DEFAULT_WORKFLOW_LIFECYCLE.terminalStates) {
|
|
@@ -1852,49 +1711,16 @@ var OrchestratorService = class {
|
|
|
1852
1711
|
}
|
|
1853
1712
|
return [...terminalStates.values()];
|
|
1854
1713
|
}
|
|
1855
|
-
|
|
1856
|
-
const
|
|
1857
|
-
for (const repository of tenant.repositories) {
|
|
1858
|
-
repositories.set(this.startupCleanupRepositoryKey(repository.owner, repository.name), repository);
|
|
1859
|
-
}
|
|
1860
|
-
for (const workspaceRecord of workspaceRecords) {
|
|
1861
|
-
const repository = this.parseWorkspaceRepositoryRef(workspaceRecord);
|
|
1862
|
-
if (!repository) {
|
|
1863
|
-
continue;
|
|
1864
|
-
}
|
|
1865
|
-
const key = this.startupCleanupRepositoryKey(repository.owner, repository.name);
|
|
1866
|
-
if (!repositories.has(key)) {
|
|
1867
|
-
repositories.set(key, repository);
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
return [...repositories.values()];
|
|
1871
|
-
}
|
|
1872
|
-
parseWorkspaceRepositoryRef(workspaceRecord) {
|
|
1873
|
-
const match = workspaceRecord.issueIdentifier.match(/^([^/]+)\/([^#]+)#\d+$/);
|
|
1874
|
-
if (!match) {
|
|
1875
|
-
return null;
|
|
1876
|
-
}
|
|
1877
|
-
const owner = match[1];
|
|
1878
|
-
const name = match[2];
|
|
1879
|
-
if (!owner || !name) {
|
|
1880
|
-
return null;
|
|
1881
|
-
}
|
|
1882
|
-
return {
|
|
1883
|
-
owner,
|
|
1884
|
-
name,
|
|
1885
|
-
cloneUrl: workspaceRecord.repositoryPath
|
|
1886
|
-
};
|
|
1887
|
-
}
|
|
1888
|
-
startupCleanupRepositoryKey(owner, name) {
|
|
1889
|
-
return `${owner}/${name}`;
|
|
1890
|
-
}
|
|
1891
|
-
async loadStartupCleanupWorkflow(tenant, repository, workflowCache) {
|
|
1892
|
-
const cacheKey = this.workflowCacheKey(repository);
|
|
1714
|
+
async loadStartupCleanupWorkflow(tenant, workflowCache) {
|
|
1715
|
+
const cacheKey = this.workflowCacheKey(tenant.repository);
|
|
1893
1716
|
const cachedResolution = workflowCache.get(cacheKey);
|
|
1894
1717
|
if (cachedResolution) {
|
|
1895
1718
|
return cachedResolution;
|
|
1896
1719
|
}
|
|
1897
|
-
const resolutionPromise =
|
|
1720
|
+
const resolutionPromise = this.loadProjectWorkflow(
|
|
1721
|
+
tenant,
|
|
1722
|
+
tenant.repository
|
|
1723
|
+
);
|
|
1898
1724
|
workflowCache.set(cacheKey, resolutionPromise);
|
|
1899
1725
|
return resolutionPromise;
|
|
1900
1726
|
}
|
|
@@ -1916,7 +1742,11 @@ var OrchestratorService = class {
|
|
|
1916
1742
|
const workflowResolutionCache = /* @__PURE__ */ new Map();
|
|
1917
1743
|
this.workflowResolutionCache = workflowResolutionCache;
|
|
1918
1744
|
try {
|
|
1919
|
-
return await this.reconcileProject(
|
|
1745
|
+
return await this.reconcileProject(
|
|
1746
|
+
this.projectConfig,
|
|
1747
|
+
issueIdentifier,
|
|
1748
|
+
trackerDependencies
|
|
1749
|
+
);
|
|
1920
1750
|
} finally {
|
|
1921
1751
|
if (this.workflowResolutionCache === workflowResolutionCache) {
|
|
1922
1752
|
this.workflowResolutionCache = null;
|
|
@@ -1931,14 +1761,21 @@ var OrchestratorService = class {
|
|
|
1931
1761
|
};
|
|
1932
1762
|
}
|
|
1933
1763
|
async findLatestRunForIssue(issueId, issueIdentifier) {
|
|
1934
|
-
const matchingRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === this.projectConfig.projectId).filter(
|
|
1764
|
+
const matchingRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === this.projectConfig.projectId).filter(
|
|
1765
|
+
(run) => run.issueId === issueId || run.issueIdentifier === issueIdentifier
|
|
1766
|
+
).sort(
|
|
1767
|
+
(left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
|
|
1768
|
+
);
|
|
1935
1769
|
return matchingRuns[0] ?? null;
|
|
1936
1770
|
}
|
|
1937
1771
|
async resolveActionableCandidates(tenant, issues) {
|
|
1938
1772
|
const candidates = [];
|
|
1939
1773
|
let lifecycle = null;
|
|
1940
1774
|
for (const issue of issues) {
|
|
1941
|
-
const resolution = await this.loadProjectWorkflow(
|
|
1775
|
+
const resolution = await this.loadProjectWorkflow(
|
|
1776
|
+
tenant,
|
|
1777
|
+
issue.repository
|
|
1778
|
+
);
|
|
1942
1779
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
1943
1780
|
continue;
|
|
1944
1781
|
}
|
|
@@ -1950,8 +1787,11 @@ var OrchestratorService = class {
|
|
|
1950
1787
|
}
|
|
1951
1788
|
candidates.push(issue);
|
|
1952
1789
|
}
|
|
1953
|
-
if (!lifecycle
|
|
1954
|
-
const resolution = await this.loadProjectWorkflow(
|
|
1790
|
+
if (!lifecycle) {
|
|
1791
|
+
const resolution = await this.loadProjectWorkflow(
|
|
1792
|
+
tenant,
|
|
1793
|
+
tenant.repository
|
|
1794
|
+
);
|
|
1955
1795
|
if (isUsableWorkflowResolution(resolution)) {
|
|
1956
1796
|
lifecycle = resolution.lifecycle;
|
|
1957
1797
|
}
|
|
@@ -1967,24 +1807,7 @@ var OrchestratorService = class {
|
|
|
1967
1807
|
};
|
|
1968
1808
|
}
|
|
1969
1809
|
isIssueCandidateEligible(issue, lifecycle, issues) {
|
|
1970
|
-
|
|
1971
|
-
return false;
|
|
1972
|
-
}
|
|
1973
|
-
if (!matchesWorkflowState(issue.state, lifecycle.blockerCheckStates) || issue.blockedBy.length === 0) {
|
|
1974
|
-
return true;
|
|
1975
|
-
}
|
|
1976
|
-
return !issue.blockedBy.some((blockerRef) => {
|
|
1977
|
-
if (blockerRef.state && isStateTerminal(blockerRef.state, lifecycle)) {
|
|
1978
|
-
return false;
|
|
1979
|
-
}
|
|
1980
|
-
if (blockerRef.identifier) {
|
|
1981
|
-
const blockerIssue = issues.find((candidate) => candidate.identifier === blockerRef.identifier);
|
|
1982
|
-
if (blockerIssue?.state) {
|
|
1983
|
-
return !isStateTerminal(blockerIssue.state, lifecycle);
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
return true;
|
|
1987
|
-
});
|
|
1810
|
+
return isIssueCandidateEligibleWithReason(issue, lifecycle, issues).eligible;
|
|
1988
1811
|
}
|
|
1989
1812
|
async loadProjectWorkflow(tenant, repository) {
|
|
1990
1813
|
const cacheKey = this.workflowCacheKey(repository);
|
|
@@ -1994,24 +1817,39 @@ var OrchestratorService = class {
|
|
|
1994
1817
|
if (cachedResolution) {
|
|
1995
1818
|
return cachedResolution;
|
|
1996
1819
|
}
|
|
1997
|
-
const resolutionPromise = this.loadProjectWorkflowUncached(
|
|
1820
|
+
const resolutionPromise = this.loadProjectWorkflowUncached(
|
|
1821
|
+
tenant,
|
|
1822
|
+
repository
|
|
1823
|
+
);
|
|
1998
1824
|
pendingCache.set(cacheKey, resolutionPromise);
|
|
1999
1825
|
return resolutionPromise;
|
|
2000
1826
|
}
|
|
2001
1827
|
return this.loadProjectWorkflowUncached(tenant, repository);
|
|
2002
1828
|
}
|
|
2003
1829
|
async loadProjectWorkflowUncached(tenant, repository) {
|
|
2004
|
-
const cacheRoot = join3(
|
|
2005
|
-
|
|
1830
|
+
const cacheRoot = join3(
|
|
1831
|
+
this.store.projectDir(tenant.projectId),
|
|
1832
|
+
"cache",
|
|
1833
|
+
repository.owner,
|
|
1834
|
+
repository.name
|
|
1835
|
+
);
|
|
1836
|
+
const repositoryDirectory = this.resolveWorkflowRepositoryDirectory(repository);
|
|
1837
|
+
const resolution = await loadRepositoryWorkflow(
|
|
1838
|
+
repositoryDirectory,
|
|
1839
|
+
repository
|
|
1840
|
+
);
|
|
1841
|
+
return this.resolveWorkflowResolution(
|
|
2006
1842
|
repository,
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
1843
|
+
cacheRoot,
|
|
1844
|
+
resolution,
|
|
1845
|
+
true
|
|
1846
|
+
);
|
|
2011
1847
|
}
|
|
2012
1848
|
async startRun(tenant, issue) {
|
|
2013
1849
|
if (this.shuttingDown || !this.running) {
|
|
2014
|
-
throw new Error(
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
"Orchestrator is shutting down and cannot start new runs."
|
|
1852
|
+
);
|
|
2015
1853
|
}
|
|
2016
1854
|
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
2017
1855
|
const now = this.now();
|
|
@@ -2020,19 +1858,34 @@ var OrchestratorService = class {
|
|
|
2020
1858
|
const workspaceRuntimeDir = runDir;
|
|
2021
1859
|
const issueSubjectId = issue.id;
|
|
2022
1860
|
const identity = {
|
|
2023
|
-
projectId: tenant.projectId,
|
|
2024
1861
|
adapter: issue.tracker.adapter,
|
|
2025
1862
|
issueSubjectId
|
|
2026
1863
|
};
|
|
2027
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
2028
|
-
|
|
2029
|
-
|
|
1864
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1865
|
+
identity,
|
|
1866
|
+
issue.identifier
|
|
1867
|
+
);
|
|
1868
|
+
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(
|
|
1869
|
+
identity,
|
|
1870
|
+
tenant.projectId
|
|
1871
|
+
);
|
|
1872
|
+
const existingWorkspaceRecord = await this.store.loadIssueWorkspace(
|
|
1873
|
+
tenant.projectId,
|
|
1874
|
+
preferredWorkspaceKey
|
|
1875
|
+
) ?? (legacyWorkspaceKey === preferredWorkspaceKey ? null : await this.store.loadIssueWorkspace(
|
|
1876
|
+
tenant.projectId,
|
|
1877
|
+
legacyWorkspaceKey
|
|
1878
|
+
));
|
|
2030
1879
|
const workspaceKey = existingWorkspaceRecord?.workspaceKey ?? preferredWorkspaceKey;
|
|
2031
1880
|
const projectDir = this.store.projectDir(tenant.projectId);
|
|
2032
|
-
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
1881
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
1882
|
+
projectDir,
|
|
1883
|
+
workspaceKey
|
|
1884
|
+
);
|
|
2033
1885
|
const repositoryDirectory = await ensureIssueWorkspaceRepository({
|
|
2034
1886
|
repository: issue.repository,
|
|
2035
|
-
issueWorkspacePath
|
|
1887
|
+
issueWorkspacePath,
|
|
1888
|
+
existingWorkspace: Boolean(existingWorkspaceRecord)
|
|
2036
1889
|
});
|
|
2037
1890
|
if (!existingWorkspaceRecord) {
|
|
2038
1891
|
const workspaceRecord = {
|
|
@@ -2049,14 +1902,20 @@ var OrchestratorService = class {
|
|
|
2049
1902
|
lastError: null
|
|
2050
1903
|
};
|
|
2051
1904
|
await this.store.saveIssueWorkspace(workspaceRecord);
|
|
2052
|
-
const afterCreateResult = await this.runHook(
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
1905
|
+
const afterCreateResult = await this.runHook(
|
|
1906
|
+
"after_create",
|
|
1907
|
+
tenant,
|
|
1908
|
+
repositoryDirectory,
|
|
1909
|
+
issue.repository,
|
|
1910
|
+
{
|
|
1911
|
+
projectId: tenant.projectId,
|
|
1912
|
+
workspaceKey,
|
|
1913
|
+
issueSubjectId,
|
|
1914
|
+
issueIdentifier: issue.identifier,
|
|
1915
|
+
workspacePath: issueWorkspacePath,
|
|
1916
|
+
repositoryPath: repositoryDirectory
|
|
1917
|
+
}
|
|
1918
|
+
);
|
|
2060
1919
|
if (afterCreateResult && afterCreateResult.outcome !== "success" && afterCreateResult.outcome !== "skipped") {
|
|
2061
1920
|
await this.store.appendRunEvent(runId, {
|
|
2062
1921
|
at: now.toISOString(),
|
|
@@ -2069,23 +1928,35 @@ var OrchestratorService = class {
|
|
|
2069
1928
|
}
|
|
2070
1929
|
const workflow = await this.loadProjectWorkflow(tenant, issue.repository);
|
|
2071
1930
|
if (!isUsableWorkflowResolution(workflow)) {
|
|
2072
|
-
throw new Error(
|
|
1931
|
+
throw new Error(
|
|
1932
|
+
workflow.validationError ?? "Invalid repository WORKFLOW.md"
|
|
1933
|
+
);
|
|
2073
1934
|
}
|
|
2074
1935
|
const promptVariables = buildPromptVariables(issue, {
|
|
2075
1936
|
attempt: null
|
|
2076
1937
|
// first execution
|
|
2077
1938
|
});
|
|
2078
|
-
const renderedPrompt = renderPrompt(
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
1939
|
+
const renderedPrompt = renderPrompt(
|
|
1940
|
+
workflow.promptTemplate,
|
|
1941
|
+
promptVariables
|
|
1942
|
+
);
|
|
1943
|
+
await this.runHook(
|
|
1944
|
+
"before_run",
|
|
1945
|
+
tenant,
|
|
1946
|
+
repositoryDirectory,
|
|
1947
|
+
issue.repository,
|
|
1948
|
+
{
|
|
1949
|
+
projectId: tenant.projectId,
|
|
1950
|
+
workspaceKey,
|
|
1951
|
+
issueSubjectId,
|
|
1952
|
+
issueIdentifier: issue.identifier,
|
|
1953
|
+
workspacePath: issueWorkspacePath,
|
|
1954
|
+
repositoryPath: repositoryDirectory,
|
|
1955
|
+
runId,
|
|
1956
|
+
state: issue.state
|
|
1957
|
+
}
|
|
1958
|
+
);
|
|
1959
|
+
const runtimeTimeouts = resolveWorkflowRuntimeTimeouts(workflow.workflow);
|
|
2089
1960
|
mkdirSync(runDir, { recursive: true });
|
|
2090
1961
|
const workerLogStream = (this.dependencies.createWriteStreamImpl ?? createWriteStream)(join3(runDir, "worker.log"), {
|
|
2091
1962
|
flags: "a"
|
|
@@ -2108,57 +1979,65 @@ var OrchestratorService = class {
|
|
|
2108
1979
|
}
|
|
2109
1980
|
workerLogAvailable = false;
|
|
2110
1981
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2111
|
-
this.writeStderr(
|
|
1982
|
+
this.writeStderr(
|
|
1983
|
+
`[orchestrator] failed to write worker log for ${runId}: ${message}`
|
|
1984
|
+
);
|
|
2112
1985
|
};
|
|
2113
|
-
const child = (this.dependencies.spawnImpl ?? spawn2)(
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
1986
|
+
const child = (this.dependencies.spawnImpl ?? spawn2)(
|
|
1987
|
+
"bash",
|
|
1988
|
+
["-lc", resolveWorkerCommand()],
|
|
1989
|
+
{
|
|
1990
|
+
cwd: process.cwd(),
|
|
1991
|
+
env: this.buildProjectExecutionEnv(tenant.projectId, {
|
|
1992
|
+
GITHUB_GRAPHQL_TOKEN: process.env.GITHUB_GRAPHQL_TOKEN ?? "",
|
|
1993
|
+
CODEX_PROJECT_ID: tenant.projectId,
|
|
1994
|
+
PROJECT_ID: tenant.projectId,
|
|
1995
|
+
WORKING_DIRECTORY: repositoryDirectory,
|
|
1996
|
+
WORKSPACE_RUNTIME_DIR: workspaceRuntimeDir,
|
|
1997
|
+
SYMPHONY_RUN_ID: runId,
|
|
1998
|
+
SYMPHONY_ISSUE_STATE: issue.state,
|
|
1999
|
+
SYMPHONY_ISSUE_ID: issue.id,
|
|
2000
|
+
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
2001
|
+
SYMPHONY_ISSUE_TITLE: issue.title,
|
|
2002
|
+
SYMPHONY_ISSUE_SUBJECT_ID: issueSubjectId,
|
|
2003
|
+
SYMPHONY_ISSUE_WORKSPACE_KEY: workspaceKey,
|
|
2004
|
+
SYMPHONY_TRACKER_ADAPTER: issue.tracker.adapter,
|
|
2005
|
+
SYMPHONY_TRACKER_BINDING_ID: issue.tracker.bindingId,
|
|
2006
|
+
SYMPHONY_TRACKER_ITEM_ID: issue.tracker.itemId,
|
|
2007
|
+
TARGET_REPOSITORY_CLONE_URL: issue.repository.cloneUrl,
|
|
2008
|
+
TARGET_REPOSITORY_OWNER: issue.repository.owner,
|
|
2009
|
+
TARGET_REPOSITORY_NAME: issue.repository.name,
|
|
2010
|
+
TARGET_REPOSITORY_URL: issue.repository.url,
|
|
2011
|
+
...trackerAdapter.buildWorkerEnvironment(tenant, issue),
|
|
2012
|
+
SYMPHONY_RENDERED_PROMPT: renderedPrompt,
|
|
2013
|
+
SYMPHONY_WORKFLOW_PATH: workflow.workflowPath ?? "",
|
|
2014
|
+
SYMPHONY_AGENT_COMMAND: resolveWorkflowRuntimeCommand(
|
|
2015
|
+
workflow.workflow
|
|
2016
|
+
),
|
|
2017
|
+
SYMPHONY_APPROVAL_POLICY: workflow.workflow.codex.approvalPolicy ?? "",
|
|
2018
|
+
SYMPHONY_THREAD_SANDBOX: workflow.workflow.codex.threadSandbox ?? "",
|
|
2019
|
+
SYMPHONY_TURN_SANDBOX_POLICY: workflow.workflow.codex.turnSandboxPolicy ?? "",
|
|
2020
|
+
SYMPHONY_MAX_TURNS: String(workflow.workflow.agent.maxTurns),
|
|
2021
|
+
SYMPHONY_MAX_NONPRODUCTIVE_TURNS: process.env.SYMPHONY_MAX_NONPRODUCTIVE_TURNS ?? String(DEFAULT_MAX_NONPRODUCTIVE_TURNS),
|
|
2022
|
+
// Clear legacy resume/budget env so fresh worker sessions do not
|
|
2023
|
+
// inherit stale process-level values.
|
|
2024
|
+
SYMPHONY_GLOBAL_MAX_TURNS: "",
|
|
2025
|
+
SYMPHONY_MAX_TOKENS: "",
|
|
2026
|
+
SYMPHONY_SESSION_TIMEOUT_MS: "",
|
|
2027
|
+
SYMPHONY_RESUME_THREAD_ID: "",
|
|
2028
|
+
SYMPHONY_CUMULATIVE_TURN_COUNT: "0",
|
|
2029
|
+
SYMPHONY_CUMULATIVE_INPUT_TOKENS: "0",
|
|
2030
|
+
SYMPHONY_CUMULATIVE_OUTPUT_TOKENS: "0",
|
|
2031
|
+
SYMPHONY_CUMULATIVE_TOTAL_TOKENS: "0",
|
|
2032
|
+
SYMPHONY_LAST_TURN_SUMMARY: "",
|
|
2033
|
+
SYMPHONY_SESSION_STARTED_AT: "",
|
|
2034
|
+
SYMPHONY_READ_TIMEOUT_MS: String(runtimeTimeouts.readTimeoutMs),
|
|
2035
|
+
SYMPHONY_TURN_TIMEOUT_MS: String(runtimeTimeouts.turnTimeoutMs)
|
|
2036
|
+
}),
|
|
2037
|
+
detached: true,
|
|
2038
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
2039
|
+
}
|
|
2040
|
+
);
|
|
2162
2041
|
const handleWorkerStderrChunk = (chunk) => {
|
|
2163
2042
|
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
2164
2043
|
if (workerLogAvailable) {
|
|
@@ -2195,7 +2074,9 @@ var OrchestratorService = class {
|
|
|
2195
2074
|
if (child.pid) {
|
|
2196
2075
|
this.retireWorkerPid(child.pid);
|
|
2197
2076
|
}
|
|
2198
|
-
this.logVerbose(
|
|
2077
|
+
this.logVerbose(
|
|
2078
|
+
`[worker-exited] ${runId} (code=${code ?? "null"}, signal=${signal ?? "null"})`
|
|
2079
|
+
);
|
|
2199
2080
|
};
|
|
2200
2081
|
const finalizeWorkerStderr = (code, signal) => {
|
|
2201
2082
|
if (workerExited || workerStderrFinalizing) {
|
|
@@ -2235,7 +2116,9 @@ var OrchestratorService = class {
|
|
|
2235
2116
|
}
|
|
2236
2117
|
child.on?.("error", (error) => {
|
|
2237
2118
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2238
|
-
this.writeStderr(
|
|
2119
|
+
this.writeStderr(
|
|
2120
|
+
`[orchestrator] worker process error for ${runId}: ${message}`
|
|
2121
|
+
);
|
|
2239
2122
|
finalizeWorkerStderr(null, null);
|
|
2240
2123
|
});
|
|
2241
2124
|
child.on?.("close", (code, signal) => {
|
|
@@ -2282,14 +2165,24 @@ var OrchestratorService = class {
|
|
|
2282
2165
|
issuesByIdentifier: /* @__PURE__ */ new Map()
|
|
2283
2166
|
};
|
|
2284
2167
|
}
|
|
2285
|
-
const issues = await trackerAdapter.fetchIssueStatesByIds(
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2168
|
+
const issues = await trackerAdapter.fetchIssueStatesByIds(
|
|
2169
|
+
tenant,
|
|
2170
|
+
activeIssueIds,
|
|
2171
|
+
{
|
|
2172
|
+
fetchImpl: this.dependencies.fetchImpl
|
|
2173
|
+
}
|
|
2174
|
+
);
|
|
2175
|
+
const issuesByIdentifier = new Map(
|
|
2176
|
+
issues.map((issue) => [issue.identifier, issue])
|
|
2177
|
+
);
|
|
2178
|
+
const issueStateByIdentifier = new Map(
|
|
2179
|
+
issues.map((issue) => [issue.identifier, issue.state])
|
|
2180
|
+
);
|
|
2290
2181
|
const syncedRuns = [];
|
|
2291
2182
|
for (const run of activeRuns) {
|
|
2292
|
-
const currentTrackerState = issueStateByIdentifier.get(
|
|
2183
|
+
const currentTrackerState = issueStateByIdentifier.get(
|
|
2184
|
+
run.issueIdentifier
|
|
2185
|
+
);
|
|
2293
2186
|
if (!currentTrackerState || currentTrackerState === run.issueState) {
|
|
2294
2187
|
syncedRuns.push(run);
|
|
2295
2188
|
continue;
|
|
@@ -2312,7 +2205,9 @@ var OrchestratorService = class {
|
|
|
2312
2205
|
if (run.processId && this.isProcessRunning(run.processId)) {
|
|
2313
2206
|
const retryPolicy = await this.loadRetryPolicy(tenant, run.repository);
|
|
2314
2207
|
const configuredStallTimeoutMs = retryPolicy?.stallTimeoutMs ?? null;
|
|
2315
|
-
const lastActivityAtMs = parseTimestampMs2(
|
|
2208
|
+
const lastActivityAtMs = parseTimestampMs2(
|
|
2209
|
+
run.lastEventAt ?? run.startedAt
|
|
2210
|
+
);
|
|
2316
2211
|
const startedAtMs = parseTimestampMs2(run.startedAt);
|
|
2317
2212
|
const elapsedSinceLastActivityMs = lastActivityAtMs === null ? null : now.getTime() - lastActivityAtMs;
|
|
2318
2213
|
const runningSinceMs = startedAtMs === null ? null : now.getTime() - startedAtMs;
|
|
@@ -2324,9 +2219,13 @@ var OrchestratorService = class {
|
|
|
2324
2219
|
const elapsedSeconds = Math.round((elapsedMs ?? 0) / 1e3);
|
|
2325
2220
|
const timeoutSeconds = Math.round((timeoutMs ?? 0) / 1e3);
|
|
2326
2221
|
if (this.isVerboseLoggingEnabled()) {
|
|
2327
|
-
this.writeStderr(
|
|
2222
|
+
this.writeStderr(
|
|
2223
|
+
`[stall-detected] ${run.runId} (elapsed=${elapsedSeconds}s > ${timeoutSeconds}s)`
|
|
2224
|
+
);
|
|
2328
2225
|
} else {
|
|
2329
|
-
this.writeStderr(
|
|
2226
|
+
this.writeStderr(
|
|
2227
|
+
`[orchestrator] stuck worker detected for ${run.runId} (elapsed ${elapsedSeconds}s > ${timeoutSeconds}s) \u2014 sending SIGTERM`
|
|
2228
|
+
);
|
|
2330
2229
|
}
|
|
2331
2230
|
this.sendSignal(run.processId, "SIGTERM");
|
|
2332
2231
|
} else {
|
|
@@ -2339,11 +2238,13 @@ var OrchestratorService = class {
|
|
|
2339
2238
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2340
2239
|
issueId: run.issueId,
|
|
2341
2240
|
identifier: run.issueIdentifier,
|
|
2342
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2241
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2242
|
+
{
|
|
2243
|
+
adapter: tenant.tracker.adapter,
|
|
2244
|
+
issueSubjectId: run.issueSubjectId
|
|
2245
|
+
},
|
|
2246
|
+
run.issueIdentifier
|
|
2247
|
+
),
|
|
2347
2248
|
state: "running",
|
|
2348
2249
|
currentRunId: run.runId,
|
|
2349
2250
|
retryEntry: null,
|
|
@@ -2361,12 +2262,29 @@ var OrchestratorService = class {
|
|
|
2361
2262
|
const workerInfo = await this.fetchWorkerRunInfo(run);
|
|
2362
2263
|
const runWithTokens = {
|
|
2363
2264
|
...run,
|
|
2364
|
-
runtimeSession: buildRuntimeSession(
|
|
2265
|
+
runtimeSession: buildRuntimeSession(
|
|
2266
|
+
run.runtimeSession,
|
|
2267
|
+
workerInfo.sessionId,
|
|
2268
|
+
workerInfo.threadId,
|
|
2269
|
+
run.status === "running" ? "failed" : run.runtimeSession?.status ?? null,
|
|
2270
|
+
run.runtimeSession?.startedAt ?? run.startedAt ?? now.toISOString(),
|
|
2271
|
+
now.toISOString(),
|
|
2272
|
+
workerInfo.exitClassification
|
|
2273
|
+
),
|
|
2365
2274
|
threadId: workerInfo.threadId ?? run.threadId ?? null,
|
|
2366
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2275
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2276
|
+
run,
|
|
2277
|
+
workerInfo.turnCount ?? null
|
|
2278
|
+
),
|
|
2367
2279
|
tokenUsage: workerInfo.tokenUsage ?? run.tokenUsage,
|
|
2368
2280
|
lastEvent: workerInfo.lastEvent ?? run.lastEvent,
|
|
2369
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
2281
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
2282
|
+
run.lastTurnSummary,
|
|
2283
|
+
resolveLastTurnSummaryCandidate(
|
|
2284
|
+
workerInfo.lastEvent,
|
|
2285
|
+
workerInfo.lastError
|
|
2286
|
+
)
|
|
2287
|
+
),
|
|
2370
2288
|
lastEventAt: workerInfo.lastEventAt ?? run.lastEventAt ?? void 0,
|
|
2371
2289
|
lastEventAtSource: workerInfo.lastEventAtSource ?? run.lastEventAtSource ?? void 0,
|
|
2372
2290
|
executionPhase: workerInfo.executionPhase ?? run.executionPhase ?? null,
|
|
@@ -2392,7 +2310,11 @@ var OrchestratorService = class {
|
|
|
2392
2310
|
recovered: false
|
|
2393
2311
|
};
|
|
2394
2312
|
}
|
|
2395
|
-
if (await this.resolveRetryRestartAction(
|
|
2313
|
+
if (await this.resolveRetryRestartAction(
|
|
2314
|
+
tenant,
|
|
2315
|
+
run,
|
|
2316
|
+
trackerDependencies
|
|
2317
|
+
) === "release") {
|
|
2396
2318
|
return this.releaseRetryingRun(runWithTokens, issueRecords, now);
|
|
2397
2319
|
}
|
|
2398
2320
|
return this.restartRun(tenant, run, issueRecords, now, workerSessionId);
|
|
@@ -2410,31 +2332,49 @@ var OrchestratorService = class {
|
|
|
2410
2332
|
runPhase: runWithTokens.runPhase ?? "failed"
|
|
2411
2333
|
};
|
|
2412
2334
|
await this.store.saveRun(completedRun);
|
|
2413
|
-
this.logVerbose(
|
|
2335
|
+
this.logVerbose(
|
|
2336
|
+
`[run-completed] ${completedRun.runId} status=${completedRun.status}`
|
|
2337
|
+
);
|
|
2414
2338
|
return {
|
|
2415
2339
|
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
2416
2340
|
recovered: false
|
|
2417
2341
|
};
|
|
2418
2342
|
}
|
|
2419
2343
|
if (run.issueWorkspaceKey) {
|
|
2420
|
-
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2344
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(
|
|
2345
|
+
this.store.projectDir(tenant.projectId),
|
|
2346
|
+
run.issueWorkspaceKey
|
|
2347
|
+
);
|
|
2348
|
+
await this.runHook(
|
|
2349
|
+
"after_run",
|
|
2350
|
+
tenant,
|
|
2351
|
+
run.workingDirectory,
|
|
2352
|
+
run.repository,
|
|
2353
|
+
{
|
|
2354
|
+
projectId: run.projectId,
|
|
2355
|
+
workspaceKey: run.issueWorkspaceKey,
|
|
2356
|
+
issueSubjectId: run.issueSubjectId,
|
|
2357
|
+
issueIdentifier: run.issueIdentifier,
|
|
2358
|
+
workspacePath: issueWorkspacePath,
|
|
2359
|
+
repositoryPath: run.workingDirectory,
|
|
2360
|
+
runId: run.runId,
|
|
2361
|
+
state: run.issueState
|
|
2362
|
+
}
|
|
2363
|
+
);
|
|
2431
2364
|
}
|
|
2432
|
-
const retryKind = await this.classifyRetryKind(
|
|
2365
|
+
const retryKind = await this.classifyRetryKind(
|
|
2366
|
+
tenant,
|
|
2367
|
+
run,
|
|
2368
|
+
trackerDependencies
|
|
2369
|
+
);
|
|
2433
2370
|
const failureRetryCount = retryKind === "failure" ? (this.resolveFailureRetryCount(issueRecords, run.issueId) ?? 0) + 1 : this.resolveFailureRetryCount(issueRecords, run.issueId) ?? 0;
|
|
2434
|
-
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2371
|
+
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
2372
|
+
tenant,
|
|
2373
|
+
run.repository
|
|
2374
|
+
);
|
|
2435
2375
|
if (retryKind === "failure" && failureRetryCount >= maxFailureRetries) {
|
|
2436
2376
|
const lastError = [
|
|
2437
|
-
`Run suppressed: ${
|
|
2377
|
+
`Run suppressed: ${MAX_FAILURE_RETRIES_EXCEEDED_REASON2}.`,
|
|
2438
2378
|
`failureRetryCount=${failureRetryCount}.`,
|
|
2439
2379
|
`maxFailureRetries=${maxFailureRetries}.`
|
|
2440
2380
|
].join(" ");
|
|
@@ -2456,18 +2396,22 @@ var OrchestratorService = class {
|
|
|
2456
2396
|
projectId: run.projectId,
|
|
2457
2397
|
issueIdentifier: run.issueIdentifier,
|
|
2458
2398
|
issueId: run.issueId,
|
|
2459
|
-
reason:
|
|
2399
|
+
reason: MAX_FAILURE_RETRIES_EXCEEDED_REASON2
|
|
2460
2400
|
});
|
|
2461
|
-
this.logVerbose(
|
|
2401
|
+
this.logVerbose(
|
|
2402
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
2403
|
+
);
|
|
2462
2404
|
return {
|
|
2463
2405
|
issueRecords: upsertIssueOrchestration(issueRecords, {
|
|
2464
2406
|
issueId: run.issueId,
|
|
2465
2407
|
identifier: run.issueIdentifier,
|
|
2466
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2408
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2409
|
+
{
|
|
2410
|
+
adapter: tenant.tracker.adapter,
|
|
2411
|
+
issueSubjectId: run.issueSubjectId
|
|
2412
|
+
},
|
|
2413
|
+
run.issueIdentifier
|
|
2414
|
+
),
|
|
2471
2415
|
state: "released",
|
|
2472
2416
|
failureRetryCount,
|
|
2473
2417
|
currentRunId: null,
|
|
@@ -2479,7 +2423,9 @@ var OrchestratorService = class {
|
|
|
2479
2423
|
}
|
|
2480
2424
|
let nextRetryAt;
|
|
2481
2425
|
if (retryKind === "continuation") {
|
|
2482
|
-
nextRetryAt = new Date(
|
|
2426
|
+
nextRetryAt = new Date(
|
|
2427
|
+
now.getTime() + CONTINUATION_RETRY_DELAY_MS
|
|
2428
|
+
).toISOString();
|
|
2483
2429
|
} else {
|
|
2484
2430
|
const retryOptions = await this.loadRetryPolicy(tenant, run.repository);
|
|
2485
2431
|
const backoffMs = this.dependencies.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
@@ -2500,16 +2446,22 @@ var OrchestratorService = class {
|
|
|
2500
2446
|
lastError: retryKind === "continuation" ? null : "Worker process exited unexpectedly."
|
|
2501
2447
|
};
|
|
2502
2448
|
await this.store.saveRun(retryRecord);
|
|
2503
|
-
this.logVerbose(
|
|
2504
|
-
|
|
2449
|
+
this.logVerbose(
|
|
2450
|
+
`[retry-scheduled] ${retryRecord.runId} kind=${retryKind} attempt=${retryRecord.attempt} nextAt=${nextRetryAt}`
|
|
2451
|
+
);
|
|
2452
|
+
this.logVerbose(
|
|
2453
|
+
`[run-completed] ${retryRecord.runId} status=${retryRecord.status}`
|
|
2454
|
+
);
|
|
2505
2455
|
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2506
2456
|
issueId: run.issueId,
|
|
2507
2457
|
identifier: run.issueIdentifier,
|
|
2508
|
-
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2458
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2459
|
+
{
|
|
2460
|
+
adapter: tenant.tracker.adapter,
|
|
2461
|
+
issueSubjectId: run.issueSubjectId
|
|
2462
|
+
},
|
|
2463
|
+
run.issueIdentifier
|
|
2464
|
+
),
|
|
2513
2465
|
state: "retry_queued",
|
|
2514
2466
|
completedOnce: retryKind === "continuation" ? true : void 0,
|
|
2515
2467
|
failureRetryCount,
|
|
@@ -2568,9 +2520,13 @@ var OrchestratorService = class {
|
|
|
2568
2520
|
if (!isOrchestratorChannelEvent(parsed)) {
|
|
2569
2521
|
return;
|
|
2570
2522
|
}
|
|
2571
|
-
void this.runSerialized(
|
|
2523
|
+
void this.runSerialized(
|
|
2524
|
+
() => this.applyWorkerChannelEvent(runId, parsed)
|
|
2525
|
+
).catch((error) => {
|
|
2572
2526
|
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2573
|
-
this.writeStderr(
|
|
2527
|
+
this.writeStderr(
|
|
2528
|
+
`[orchestrator] failed to apply worker channel event for ${runId}: ${message}`
|
|
2529
|
+
);
|
|
2574
2530
|
});
|
|
2575
2531
|
} catch {
|
|
2576
2532
|
}
|
|
@@ -2587,15 +2543,29 @@ var OrchestratorService = class {
|
|
|
2587
2543
|
...run,
|
|
2588
2544
|
updatedAt: nowIso2,
|
|
2589
2545
|
lastEvent: "heartbeat",
|
|
2590
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
2546
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
2547
|
+
run.lastTurnSummary,
|
|
2548
|
+
event.lastError
|
|
2549
|
+
),
|
|
2591
2550
|
lastEventAt: persistedLastEventAt,
|
|
2592
2551
|
lastEventAtSource: event.lastEventAt != null ? "event-channel" : run.lastEventAtSource ?? null,
|
|
2593
2552
|
tokenUsage: event.tokenUsage,
|
|
2594
2553
|
rateLimits: event.rateLimits,
|
|
2595
|
-
runtimeSession: buildRuntimeSession(
|
|
2554
|
+
runtimeSession: buildRuntimeSession(
|
|
2555
|
+
run.runtimeSession,
|
|
2556
|
+
resolveChannelSessionId(event.sessionInfo),
|
|
2557
|
+
event.sessionInfo?.threadId ?? null,
|
|
2558
|
+
"active",
|
|
2559
|
+
run.startedAt ?? run.runtimeSession?.startedAt ?? nowIso2,
|
|
2560
|
+
nowIso2,
|
|
2561
|
+
event.sessionInfo?.exitClassification ?? null
|
|
2562
|
+
),
|
|
2596
2563
|
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2597
2564
|
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
2598
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2565
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2566
|
+
run,
|
|
2567
|
+
event.sessionInfo?.turnCount ?? null
|
|
2568
|
+
),
|
|
2599
2569
|
executionPhase: event.executionPhase ?? run.executionPhase,
|
|
2600
2570
|
runPhase: event.runPhase ?? run.runPhase,
|
|
2601
2571
|
lastError: event.lastError
|
|
@@ -2656,15 +2626,29 @@ var OrchestratorService = class {
|
|
|
2656
2626
|
...run,
|
|
2657
2627
|
updatedAt: nowIso,
|
|
2658
2628
|
lastEvent: event.event ?? run.lastEvent ?? null,
|
|
2659
|
-
lastTurnSummary: resolveLastTurnSummary(
|
|
2629
|
+
lastTurnSummary: resolveLastTurnSummary(
|
|
2630
|
+
run.lastTurnSummary,
|
|
2631
|
+
resolveLastTurnSummaryCandidate(event.event, event.lastError)
|
|
2632
|
+
),
|
|
2660
2633
|
lastEventAt: event.lastEventAt,
|
|
2661
2634
|
lastEventAtSource: "event-channel",
|
|
2662
2635
|
tokenUsage: event.tokenUsage ?? run.tokenUsage,
|
|
2663
2636
|
rateLimits: event.rateLimits ?? run.rateLimits ?? null,
|
|
2664
|
-
runtimeSession: buildRuntimeSession(
|
|
2637
|
+
runtimeSession: buildRuntimeSession(
|
|
2638
|
+
run.runtimeSession,
|
|
2639
|
+
resolveChannelSessionId(event.sessionInfo),
|
|
2640
|
+
event.sessionInfo?.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2641
|
+
"active",
|
|
2642
|
+
run.startedAt ?? run.runtimeSession?.startedAt ?? nowIso,
|
|
2643
|
+
nowIso,
|
|
2644
|
+
event.sessionInfo?.exitClassification ?? null
|
|
2645
|
+
),
|
|
2665
2646
|
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2666
2647
|
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
2667
|
-
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2648
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(
|
|
2649
|
+
run,
|
|
2650
|
+
event.sessionInfo?.turnCount ?? null
|
|
2651
|
+
),
|
|
2668
2652
|
executionPhase: event.executionPhase ?? run.executionPhase ?? null,
|
|
2669
2653
|
runPhase: event.runPhase ?? run.runPhase ?? null,
|
|
2670
2654
|
lastError: event.lastError ?? run.lastError
|
|
@@ -2729,7 +2713,11 @@ var OrchestratorService = class {
|
|
|
2729
2713
|
*/
|
|
2730
2714
|
async classifyRetryKind(tenant, run, trackerDependencies = {}) {
|
|
2731
2715
|
try {
|
|
2732
|
-
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2716
|
+
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2717
|
+
tenant,
|
|
2718
|
+
run.issueIdentifier,
|
|
2719
|
+
trackerDependencies
|
|
2720
|
+
);
|
|
2733
2721
|
if (!eligibleContext) {
|
|
2734
2722
|
return "failure";
|
|
2735
2723
|
}
|
|
@@ -2737,14 +2725,22 @@ var OrchestratorService = class {
|
|
|
2737
2725
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
2738
2726
|
return "failure";
|
|
2739
2727
|
}
|
|
2740
|
-
return this.isIssueCandidateEligible(
|
|
2728
|
+
return this.isIssueCandidateEligible(
|
|
2729
|
+
eligibleContext.issue,
|
|
2730
|
+
resolution.lifecycle,
|
|
2731
|
+
eligibleContext.issues
|
|
2732
|
+
) ? "continuation" : "failure";
|
|
2741
2733
|
} catch {
|
|
2742
2734
|
return "failure";
|
|
2743
2735
|
}
|
|
2744
2736
|
}
|
|
2745
2737
|
async resolveRetryRestartAction(tenant, run, trackerDependencies = {}) {
|
|
2746
2738
|
try {
|
|
2747
|
-
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2739
|
+
const eligibleContext = await this.fetchTrackedIssueEligibilityContext(
|
|
2740
|
+
tenant,
|
|
2741
|
+
run.issueIdentifier,
|
|
2742
|
+
trackerDependencies
|
|
2743
|
+
);
|
|
2748
2744
|
if (!eligibleContext) {
|
|
2749
2745
|
return "release";
|
|
2750
2746
|
}
|
|
@@ -2752,7 +2748,11 @@ var OrchestratorService = class {
|
|
|
2752
2748
|
if (!isUsableWorkflowResolution(resolution)) {
|
|
2753
2749
|
return "restart";
|
|
2754
2750
|
}
|
|
2755
|
-
return this.isIssueCandidateEligible(
|
|
2751
|
+
return this.isIssueCandidateEligible(
|
|
2752
|
+
eligibleContext.issue,
|
|
2753
|
+
resolution.lifecycle,
|
|
2754
|
+
eligibleContext.issues
|
|
2755
|
+
) ? "restart" : "release";
|
|
2756
2756
|
} catch {
|
|
2757
2757
|
return "restart";
|
|
2758
2758
|
}
|
|
@@ -2763,7 +2763,9 @@ var OrchestratorService = class {
|
|
|
2763
2763
|
fetchImpl: this.dependencies.fetchImpl,
|
|
2764
2764
|
...trackerDependencies
|
|
2765
2765
|
});
|
|
2766
|
-
const issue = issues.find(
|
|
2766
|
+
const issue = issues.find(
|
|
2767
|
+
(candidate) => candidate.identifier === issueIdentifier
|
|
2768
|
+
);
|
|
2767
2769
|
return issue ? { issue, issues } : null;
|
|
2768
2770
|
}
|
|
2769
2771
|
async fetchWorkerRunInfo(run) {
|
|
@@ -2787,12 +2789,20 @@ var OrchestratorService = class {
|
|
|
2787
2789
|
async readPersistedWorkerTokenUsage(run) {
|
|
2788
2790
|
const artifactPaths = [
|
|
2789
2791
|
join3(run.workspaceRuntimeDir, "token-usage.json"),
|
|
2790
|
-
join3(
|
|
2792
|
+
join3(
|
|
2793
|
+
run.workspaceRuntimeDir,
|
|
2794
|
+
".orchestrator",
|
|
2795
|
+
"runs",
|
|
2796
|
+
run.runId,
|
|
2797
|
+
"token-usage.json"
|
|
2798
|
+
)
|
|
2791
2799
|
];
|
|
2792
2800
|
for (const artifactPath of artifactPaths) {
|
|
2793
2801
|
try {
|
|
2794
2802
|
const raw = await readFile3(artifactPath, "utf8");
|
|
2795
|
-
const tokenUsage = JSON.parse(
|
|
2803
|
+
const tokenUsage = JSON.parse(
|
|
2804
|
+
raw
|
|
2805
|
+
);
|
|
2796
2806
|
if (hasTokenUsage(tokenUsage)) {
|
|
2797
2807
|
return tokenUsage;
|
|
2798
2808
|
}
|
|
@@ -2813,7 +2823,10 @@ var OrchestratorService = class {
|
|
|
2813
2823
|
if (!isUsableWorkflowResolution(workflowResolution)) {
|
|
2814
2824
|
return null;
|
|
2815
2825
|
}
|
|
2816
|
-
const hookEnv = this.buildProjectExecutionEnv(
|
|
2826
|
+
const hookEnv = this.buildProjectExecutionEnv(
|
|
2827
|
+
tenant.projectId,
|
|
2828
|
+
buildHookEnv(context)
|
|
2829
|
+
);
|
|
2817
2830
|
return executeWorkspaceHook({
|
|
2818
2831
|
kind,
|
|
2819
2832
|
hooks: workflowResolution.workflow.hooks,
|
|
@@ -2831,14 +2844,24 @@ var OrchestratorService = class {
|
|
|
2831
2844
|
return readEnvFile(envPath);
|
|
2832
2845
|
} catch (error) {
|
|
2833
2846
|
const message = error instanceof Error ? error.message : "Unknown error occurred.";
|
|
2834
|
-
(this.dependencies.stderr ?? process.stderr).write(
|
|
2835
|
-
`
|
|
2847
|
+
(this.dependencies.stderr ?? process.stderr).write(
|
|
2848
|
+
`[warn] Failed to load project env for ${projectId} from ${envPath}: ${message}
|
|
2849
|
+
`
|
|
2850
|
+
);
|
|
2836
2851
|
return {};
|
|
2837
2852
|
}
|
|
2838
2853
|
}
|
|
2839
2854
|
buildProjectExecutionEnv(projectId, env) {
|
|
2840
|
-
const inheritedEnv = Object.fromEntries(
|
|
2841
|
-
|
|
2855
|
+
const inheritedEnv = Object.fromEntries(
|
|
2856
|
+
Object.entries(process.env).filter(
|
|
2857
|
+
(entry) => typeof entry[1] === "string"
|
|
2858
|
+
)
|
|
2859
|
+
);
|
|
2860
|
+
const explicitEnv = Object.fromEntries(
|
|
2861
|
+
Object.entries(env).filter(
|
|
2862
|
+
(entry) => typeof entry[1] === "string"
|
|
2863
|
+
)
|
|
2864
|
+
);
|
|
2842
2865
|
return {
|
|
2843
2866
|
...this.readProjectEnv(projectId),
|
|
2844
2867
|
...inheritedEnv,
|
|
@@ -2854,7 +2877,10 @@ var OrchestratorService = class {
|
|
|
2854
2877
|
lastError: "Superseded by recovered run."
|
|
2855
2878
|
};
|
|
2856
2879
|
await this.store.saveRun(supersededRecord);
|
|
2857
|
-
const issue = resolveTrackerAdapter2(tenant.tracker).reviveIssue(
|
|
2880
|
+
const issue = resolveTrackerAdapter2(tenant.tracker).reviveIssue(
|
|
2881
|
+
tenant,
|
|
2882
|
+
run
|
|
2883
|
+
);
|
|
2858
2884
|
const restarted = await this.startRun(tenant, issue);
|
|
2859
2885
|
const recoveredRecord = {
|
|
2860
2886
|
...restarted,
|
|
@@ -2880,11 +2906,13 @@ var OrchestratorService = class {
|
|
|
2880
2906
|
issueRecords: upsertIssueOrchestration(issueRecords, {
|
|
2881
2907
|
issueId: recoveredRecord.issueId,
|
|
2882
2908
|
identifier: recoveredRecord.issueIdentifier,
|
|
2883
|
-
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2909
|
+
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2910
|
+
{
|
|
2911
|
+
adapter: tenant.tracker.adapter,
|
|
2912
|
+
issueSubjectId: recoveredRecord.issueSubjectId
|
|
2913
|
+
},
|
|
2914
|
+
recoveredRecord.issueIdentifier
|
|
2915
|
+
),
|
|
2888
2916
|
state: "running",
|
|
2889
2917
|
currentRunId: recoveredRecord.runId,
|
|
2890
2918
|
retryEntry: null,
|
|
@@ -2905,40 +2933,34 @@ var OrchestratorService = class {
|
|
|
2905
2933
|
lastError: "Retry canceled because the tracker issue is no longer actionable."
|
|
2906
2934
|
};
|
|
2907
2935
|
await this.store.saveRun(suppressedRun);
|
|
2908
|
-
this.logVerbose(
|
|
2936
|
+
this.logVerbose(
|
|
2937
|
+
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
2938
|
+
);
|
|
2909
2939
|
return {
|
|
2910
2940
|
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
2911
2941
|
recovered: false
|
|
2912
2942
|
};
|
|
2913
2943
|
}
|
|
2914
2944
|
async loadProjectPollInterval(tenant) {
|
|
2915
|
-
const
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
const
|
|
2920
|
-
return
|
|
2945
|
+
const resolution = await this.loadProjectWorkflow(
|
|
2946
|
+
tenant,
|
|
2947
|
+
tenant.repository
|
|
2948
|
+
);
|
|
2949
|
+
const interval = isUsableWorkflowResolution(resolution) ? resolution.workflow.polling.intervalMs : NaN;
|
|
2950
|
+
return Number.isFinite(interval) && interval > 0 ? interval : DEFAULT_POLL_INTERVAL_MS;
|
|
2921
2951
|
}
|
|
2922
2952
|
async loadProjectMaxConcurrentByState(tenant) {
|
|
2923
2953
|
const result = {};
|
|
2924
|
-
const
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
}
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
if (!isUsableWorkflowResolution(resolution))
|
|
2935
|
-
continue;
|
|
2936
|
-
const stateLimits = resolution.workflow.agent.maxConcurrentAgentsByState;
|
|
2937
|
-
for (const [state, limit] of Object.entries(stateLimits)) {
|
|
2938
|
-
const existing = result[state];
|
|
2939
|
-
const numLimit = typeof limit === "number" ? limit : Number(limit);
|
|
2940
|
-
result[state] = existing === void 0 ? numLimit : Math.min(existing, numLimit);
|
|
2941
|
-
}
|
|
2954
|
+
const resolution = await this.loadProjectWorkflow(
|
|
2955
|
+
tenant,
|
|
2956
|
+
tenant.repository
|
|
2957
|
+
).catch(() => null);
|
|
2958
|
+
if (!resolution || !isUsableWorkflowResolution(resolution)) {
|
|
2959
|
+
return result;
|
|
2960
|
+
}
|
|
2961
|
+
const stateLimits = resolution.workflow.agent.maxConcurrentAgentsByState;
|
|
2962
|
+
for (const [state, limit] of Object.entries(stateLimits)) {
|
|
2963
|
+
result[state] = typeof limit === "number" ? limit : Number(limit);
|
|
2942
2964
|
}
|
|
2943
2965
|
return result;
|
|
2944
2966
|
}
|
|
@@ -2951,7 +2973,7 @@ var OrchestratorService = class {
|
|
|
2951
2973
|
return {
|
|
2952
2974
|
baseDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.retryBaseDelayMs,
|
|
2953
2975
|
maxDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.maxRetryBackoffMs,
|
|
2954
|
-
stallTimeoutMs: resolution.workflow.
|
|
2976
|
+
stallTimeoutMs: resolveWorkflowRuntimeTimeouts(resolution.workflow).stallTimeoutMs
|
|
2955
2977
|
};
|
|
2956
2978
|
} catch {
|
|
2957
2979
|
if (!this.dependencies.retryBackoffMs) {
|
|
@@ -2968,16 +2990,10 @@ var OrchestratorService = class {
|
|
|
2968
2990
|
if (this.dependencies.concurrency !== void 0) {
|
|
2969
2991
|
return this.dependencies.concurrency;
|
|
2970
2992
|
}
|
|
2971
|
-
const
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
} catch {
|
|
2976
|
-
return NaN;
|
|
2977
|
-
}
|
|
2978
|
-
}));
|
|
2979
|
-
const validLimits = limits.filter((value) => Number.isFinite(value) && value >= 0);
|
|
2980
|
-
return validLimits.length ? Math.min(...validLimits) : DEFAULT_CONCURRENCY;
|
|
2993
|
+
const limit = await this.loadProjectWorkflow(project, project.repository).then(
|
|
2994
|
+
(resolution) => isUsableWorkflowResolution(resolution) ? resolution.workflow.agent.maxConcurrentAgents : NaN
|
|
2995
|
+
).catch(() => NaN);
|
|
2996
|
+
return Number.isFinite(limit) && limit >= 0 ? limit : DEFAULT_CONCURRENCY;
|
|
2981
2997
|
}
|
|
2982
2998
|
async resolveWorkflowResolution(repository, cacheRoot, resolution, changed) {
|
|
2983
2999
|
const cacheKey = this.workflowCacheKey(repository);
|
|
@@ -2990,7 +3006,10 @@ var OrchestratorService = class {
|
|
|
2990
3006
|
};
|
|
2991
3007
|
let workflowPath = effectiveResolution.workflowPath;
|
|
2992
3008
|
try {
|
|
2993
|
-
workflowPath = await this.persistLastKnownGoodWorkflow(
|
|
3009
|
+
workflowPath = await this.persistLastKnownGoodWorkflow(
|
|
3010
|
+
cacheRoot,
|
|
3011
|
+
effectiveResolution
|
|
3012
|
+
) ?? effectiveResolution.workflowPath;
|
|
2994
3013
|
} catch {
|
|
2995
3014
|
workflowPath = effectiveResolution.workflowPath;
|
|
2996
3015
|
}
|
|
@@ -3005,8 +3024,10 @@ var OrchestratorService = class {
|
|
|
3005
3024
|
const message = resolution.validationError ?? "Invalid repository WORKFLOW.md";
|
|
3006
3025
|
const previousMessage = this.lastReportedWorkflowErrors.get(cacheKey);
|
|
3007
3026
|
if (changed || previousMessage !== message) {
|
|
3008
|
-
process.stderr.write(
|
|
3009
|
-
`
|
|
3027
|
+
process.stderr.write(
|
|
3028
|
+
`[orchestrator] failed to reload WORKFLOW.md for ${repository.owner}/${repository.name}: ${message}
|
|
3029
|
+
`
|
|
3030
|
+
);
|
|
3010
3031
|
this.lastReportedWorkflowErrors.set(cacheKey, message);
|
|
3011
3032
|
}
|
|
3012
3033
|
if (!cached) {
|
|
@@ -3036,6 +3057,26 @@ var OrchestratorService = class {
|
|
|
3036
3057
|
workflowCacheKey(repository) {
|
|
3037
3058
|
return `${repository.owner}/${repository.name}:${this.normalizeRepositoryCloneUrl(repository.cloneUrl)}`;
|
|
3038
3059
|
}
|
|
3060
|
+
resolveWorkflowRepositoryDirectory(repository) {
|
|
3061
|
+
if (repository.path) {
|
|
3062
|
+
return repository.path;
|
|
3063
|
+
}
|
|
3064
|
+
const localCloneUrlPath = this.resolveLocalCloneUrlPath(
|
|
3065
|
+
repository.cloneUrl
|
|
3066
|
+
);
|
|
3067
|
+
if (localCloneUrlPath) {
|
|
3068
|
+
return localCloneUrlPath;
|
|
3069
|
+
}
|
|
3070
|
+
return process.cwd();
|
|
3071
|
+
}
|
|
3072
|
+
resolveLocalCloneUrlPath(cloneUrl) {
|
|
3073
|
+
try {
|
|
3074
|
+
const url = new URL(cloneUrl);
|
|
3075
|
+
return url.protocol === "file:" ? fileURLToPath(url) : null;
|
|
3076
|
+
} catch {
|
|
3077
|
+
return isAbsolute(cloneUrl) || cloneUrl.startsWith(".") ? cloneUrl : null;
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3039
3080
|
normalizeRepositoryCloneUrl(cloneUrl) {
|
|
3040
3081
|
if (cloneUrl.startsWith("file://")) {
|
|
3041
3082
|
try {
|
|
@@ -3092,14 +3133,28 @@ var OrchestratorService = class {
|
|
|
3092
3133
|
async cleanupTerminalIssueWorkspace(tenant, issue, now, workflowResolution) {
|
|
3093
3134
|
const issueSubjectId = issue.id;
|
|
3094
3135
|
const identity = {
|
|
3095
|
-
projectId: tenant.projectId,
|
|
3096
3136
|
adapter: issue.tracker.adapter,
|
|
3097
3137
|
issueSubjectId
|
|
3098
3138
|
};
|
|
3099
|
-
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
3100
|
-
|
|
3139
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
3140
|
+
identity,
|
|
3141
|
+
issue.identifier
|
|
3142
|
+
);
|
|
3143
|
+
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(
|
|
3144
|
+
identity,
|
|
3145
|
+
tenant.projectId
|
|
3146
|
+
);
|
|
3101
3147
|
const orchestrationRecord = (await this.store.loadProjectIssueOrchestrations(tenant.projectId)).find((record) => record.issueId === issue.id);
|
|
3102
|
-
const workspaceRecord = (orchestrationRecord ? await this.store.loadIssueWorkspace(
|
|
3148
|
+
const workspaceRecord = (orchestrationRecord ? await this.store.loadIssueWorkspace(
|
|
3149
|
+
tenant.projectId,
|
|
3150
|
+
orchestrationRecord.workspaceKey
|
|
3151
|
+
) : null) ?? await this.store.loadIssueWorkspace(
|
|
3152
|
+
tenant.projectId,
|
|
3153
|
+
preferredWorkspaceKey
|
|
3154
|
+
) ?? (legacyWorkspaceKey === preferredWorkspaceKey ? null : await this.store.loadIssueWorkspace(
|
|
3155
|
+
tenant.projectId,
|
|
3156
|
+
legacyWorkspaceKey
|
|
3157
|
+
));
|
|
3103
3158
|
if (!workspaceRecord || workspaceRecord.status === "removed") {
|
|
3104
3159
|
return;
|
|
3105
3160
|
}
|
|
@@ -3109,17 +3164,26 @@ var OrchestratorService = class {
|
|
|
3109
3164
|
updatedAt: now.toISOString()
|
|
3110
3165
|
};
|
|
3111
3166
|
await this.store.saveIssueWorkspace(pendingRecord);
|
|
3112
|
-
const hookResult = await this.runHook(
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3167
|
+
const hookResult = await this.runHook(
|
|
3168
|
+
"before_remove",
|
|
3169
|
+
tenant,
|
|
3170
|
+
workspaceRecord.repositoryPath,
|
|
3171
|
+
issue.repository,
|
|
3172
|
+
{
|
|
3173
|
+
projectId: tenant.projectId,
|
|
3174
|
+
workspaceKey: workspaceRecord.workspaceKey,
|
|
3175
|
+
issueSubjectId,
|
|
3176
|
+
issueIdentifier: issue.identifier,
|
|
3177
|
+
workspacePath: workspaceRecord.workspacePath,
|
|
3178
|
+
repositoryPath: workspaceRecord.repositoryPath
|
|
3179
|
+
},
|
|
3180
|
+
workflowResolution
|
|
3181
|
+
);
|
|
3120
3182
|
if (hookResult && hookResult.outcome !== "success" && hookResult.outcome !== "skipped") {
|
|
3121
3183
|
const errorMessage = hookResult.error ?? `before_remove hook ${hookResult.outcome}`;
|
|
3122
|
-
console.warn(
|
|
3184
|
+
console.warn(
|
|
3185
|
+
`[orchestrator] before_remove hook failed for ${issue.identifier}; continuing cleanup: ${errorMessage}`
|
|
3186
|
+
);
|
|
3123
3187
|
}
|
|
3124
3188
|
try {
|
|
3125
3189
|
await rm3(workspaceRecord.workspacePath, { recursive: true, force: true });
|
|
@@ -3137,19 +3201,26 @@ var OrchestratorService = class {
|
|
|
3137
3201
|
return issueRecords.find((record) => record.issueId === issueId)?.failureRetryCount ?? null;
|
|
3138
3202
|
}
|
|
3139
3203
|
async isFailureRetrySuppressedIssue(tenant, issue, issueRecords, latestRun) {
|
|
3140
|
-
const issueRecord = issueRecords.find(
|
|
3204
|
+
const issueRecord = issueRecords.find(
|
|
3205
|
+
(record) => record.issueId === issue.id || record.identifier === issue.identifier
|
|
3206
|
+
) ?? null;
|
|
3141
3207
|
if (!issueRecord || issueRecord.failureRetryCount <= 0) {
|
|
3142
3208
|
return false;
|
|
3143
3209
|
}
|
|
3144
|
-
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
3210
|
+
const maxFailureRetries = await this.loadMaxFailureRetries(
|
|
3211
|
+
tenant,
|
|
3212
|
+
issue.repository
|
|
3213
|
+
);
|
|
3145
3214
|
if (issueRecord.failureRetryCount < maxFailureRetries) {
|
|
3146
3215
|
return false;
|
|
3147
3216
|
}
|
|
3148
|
-
if (!latestRun || latestRun.status !== "suppressed" || latestRun.issueState !== issue.state || !latestRun.lastError?.includes(
|
|
3217
|
+
if (!latestRun || latestRun.status !== "suppressed" || latestRun.issueState !== issue.state || !latestRun.lastError?.includes(MAX_FAILURE_RETRIES_EXCEEDED_REASON2)) {
|
|
3149
3218
|
return false;
|
|
3150
3219
|
}
|
|
3151
3220
|
const issueUpdatedAtMs = parseTimestampMs2(issue.updatedAt);
|
|
3152
|
-
const suppressedAtMs = parseTimestampMs2(
|
|
3221
|
+
const suppressedAtMs = parseTimestampMs2(
|
|
3222
|
+
latestRun.completedAt ?? latestRun.updatedAt
|
|
3223
|
+
);
|
|
3153
3224
|
if (issueUpdatedAtMs === null || suppressedAtMs === null) {
|
|
3154
3225
|
return true;
|
|
3155
3226
|
}
|
|
@@ -3165,7 +3236,9 @@ var OrchestratorService = class {
|
|
|
3165
3236
|
}
|
|
3166
3237
|
};
|
|
3167
3238
|
function hasTokenUsage(tokenUsage) {
|
|
3168
|
-
return Boolean(
|
|
3239
|
+
return Boolean(
|
|
3240
|
+
tokenUsage && (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0 || tokenUsage.totalTokens > 0)
|
|
3241
|
+
);
|
|
3169
3242
|
}
|
|
3170
3243
|
function isRecord(value) {
|
|
3171
3244
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -3177,7 +3250,9 @@ function resolveProjectRateLimits(runs, issues) {
|
|
|
3177
3250
|
if (!isRecord(run.rateLimits)) {
|
|
3178
3251
|
continue;
|
|
3179
3252
|
}
|
|
3180
|
-
const timestamp = parseTimestampMs2(
|
|
3253
|
+
const timestamp = parseTimestampMs2(
|
|
3254
|
+
run.lastEventAt ?? run.updatedAt ?? run.startedAt
|
|
3255
|
+
);
|
|
3181
3256
|
const sortableTimestamp = timestamp ?? -Infinity;
|
|
3182
3257
|
if (sortableTimestamp >= latestRunTimestamp) {
|
|
3183
3258
|
latestRunTimestamp = sortableTimestamp;
|
|
@@ -3255,18 +3330,6 @@ function buildRuntimeSession(existing, sessionId, threadId, status, startedAt, u
|
|
|
3255
3330
|
function resolvePersistedCumulativeTurnCount(run) {
|
|
3256
3331
|
return run.cumulativeTurnCount ?? run.turnCount ?? 0;
|
|
3257
3332
|
}
|
|
3258
|
-
function hasConvergenceLockedRun(runs, issueId, issueState, issueUpdatedAt) {
|
|
3259
|
-
const latestRun = runs.filter((run) => run.issueId === issueId).sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime())[0];
|
|
3260
|
-
if (latestRun?.runtimeSession?.exitClassification !== "convergence-detected" || latestRun.issueState !== issueState) {
|
|
3261
|
-
return false;
|
|
3262
|
-
}
|
|
3263
|
-
const convergedAtMs = parseTimestampMs2(latestRun.completedAt ?? latestRun.updatedAt);
|
|
3264
|
-
const issueUpdatedAtMs = parseTimestampMs2(issueUpdatedAt);
|
|
3265
|
-
if (convergedAtMs === null || issueUpdatedAtMs === null) {
|
|
3266
|
-
return true;
|
|
3267
|
-
}
|
|
3268
|
-
return issueUpdatedAtMs <= convergedAtMs;
|
|
3269
|
-
}
|
|
3270
3333
|
function resolveCumulativeTurnCount(run, turnCount) {
|
|
3271
3334
|
const carriedTotal = resolvePersistedCumulativeTurnCount(run);
|
|
3272
3335
|
if (turnCount === null) {
|
|
@@ -3309,7 +3372,10 @@ function resolveWorkerCommand() {
|
|
|
3309
3372
|
return `node ${fileURLToPath(workerUrl)}`;
|
|
3310
3373
|
} catch {
|
|
3311
3374
|
try {
|
|
3312
|
-
const bundledWorker = join3(
|
|
3375
|
+
const bundledWorker = join3(
|
|
3376
|
+
fileURLToPath(new URL(".", import.meta.url)),
|
|
3377
|
+
"worker-entry.js"
|
|
3378
|
+
);
|
|
3313
3379
|
return `node ${bundledWorker}`;
|
|
3314
3380
|
} catch {
|
|
3315
3381
|
return DEFAULT_WORKER_COMMAND;
|
|
@@ -3322,17 +3388,13 @@ function createStore(runtimeRoot = ".runtime", options = {}) {
|
|
|
3322
3388
|
function sortCandidatesForDispatch(candidates) {
|
|
3323
3389
|
return [...candidates].sort((a, b) => {
|
|
3324
3390
|
if (a.priority !== b.priority) {
|
|
3325
|
-
if (a.priority === null)
|
|
3326
|
-
|
|
3327
|
-
if (b.priority === null)
|
|
3328
|
-
return -1;
|
|
3391
|
+
if (a.priority === null) return 1;
|
|
3392
|
+
if (b.priority === null) return -1;
|
|
3329
3393
|
return a.priority - b.priority;
|
|
3330
3394
|
}
|
|
3331
3395
|
if (a.createdAt !== b.createdAt) {
|
|
3332
|
-
if (a.createdAt === null)
|
|
3333
|
-
|
|
3334
|
-
if (b.createdAt === null)
|
|
3335
|
-
return -1;
|
|
3396
|
+
if (a.createdAt === null) return 1;
|
|
3397
|
+
if (b.createdAt === null) return -1;
|
|
3336
3398
|
return a.createdAt < b.createdAt ? -1 : 1;
|
|
3337
3399
|
}
|
|
3338
3400
|
return a.identifier.localeCompare(b.identifier);
|
|
@@ -3381,12 +3443,11 @@ function buildLatestRunMapByIssueId(runs) {
|
|
|
3381
3443
|
}
|
|
3382
3444
|
return latestRuns;
|
|
3383
3445
|
}
|
|
3384
|
-
function isIssueOrchestrationClaimed(state) {
|
|
3385
|
-
return state === "claimed" || state === "running" || state === "retry_queued";
|
|
3386
|
-
}
|
|
3387
3446
|
function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
3388
3447
|
const existingRecord = issueRecords.find((record) => record.issueId === nextRecord.issueId) ?? null;
|
|
3389
|
-
const remaining = issueRecords.filter(
|
|
3448
|
+
const remaining = issueRecords.filter(
|
|
3449
|
+
(record) => record.issueId !== nextRecord.issueId
|
|
3450
|
+
);
|
|
3390
3451
|
return [
|
|
3391
3452
|
...remaining,
|
|
3392
3453
|
{
|
|
@@ -3397,22 +3458,21 @@ function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
|
3397
3458
|
];
|
|
3398
3459
|
}
|
|
3399
3460
|
function releaseIssueOrchestration(issueRecords, issueId, now) {
|
|
3400
|
-
return issueRecords.map(
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
}
|
|
3408
|
-
|
|
3409
|
-
return status === "pending" || status === "starting" || status === "running" || status === "retrying";
|
|
3461
|
+
return issueRecords.map(
|
|
3462
|
+
(record) => record.issueId === issueId ? {
|
|
3463
|
+
...record,
|
|
3464
|
+
state: "released",
|
|
3465
|
+
currentRunId: null,
|
|
3466
|
+
retryEntry: null,
|
|
3467
|
+
updatedAt: now.toISOString()
|
|
3468
|
+
} : record
|
|
3469
|
+
);
|
|
3410
3470
|
}
|
|
3411
3471
|
|
|
3412
|
-
// ../orchestrator/
|
|
3472
|
+
// ../orchestrator/src/lock.ts
|
|
3413
3473
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3414
3474
|
import { mkdir as mkdir4, open as open2, readFile as readFile4, rm as rm4 } from "fs/promises";
|
|
3415
|
-
import { dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
3475
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
3416
3476
|
import { setTimeout as delay } from "timers/promises";
|
|
3417
3477
|
var LOCK_READ_RETRY_DELAY_MS = 10;
|
|
3418
3478
|
var LOCK_READ_RETRY_LIMIT = 20;
|
|
@@ -3447,14 +3507,18 @@ async function acquireProjectLock(input) {
|
|
|
3447
3507
|
if (existing.status === "invalid") {
|
|
3448
3508
|
invalidReadAttempts += 1;
|
|
3449
3509
|
if (invalidReadAttempts >= LOCK_READ_RETRY_LIMIT) {
|
|
3450
|
-
throw new Error(
|
|
3510
|
+
throw new Error(
|
|
3511
|
+
`Project "${input.projectId}" lock file is unreadable at "${lockPath}".`
|
|
3512
|
+
);
|
|
3451
3513
|
}
|
|
3452
3514
|
await delay(LOCK_READ_RETRY_DELAY_MS);
|
|
3453
3515
|
continue;
|
|
3454
3516
|
}
|
|
3455
3517
|
invalidReadAttempts = 0;
|
|
3456
3518
|
if ((input.isProcessRunning ?? isProcessRunning)(existing.record.pid)) {
|
|
3457
|
-
throw new Error(
|
|
3519
|
+
throw new Error(
|
|
3520
|
+
`Project "${input.projectId}" is already running (PID ${existing.record.pid}).`
|
|
3521
|
+
);
|
|
3458
3522
|
}
|
|
3459
3523
|
await rm4(lockPath, { force: true });
|
|
3460
3524
|
}
|
|
@@ -3493,16 +3557,20 @@ async function readProjectLock(lockPath) {
|
|
|
3493
3557
|
}
|
|
3494
3558
|
function assertValidProjectId(projectId) {
|
|
3495
3559
|
if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
|
|
3496
|
-
throw new Error(
|
|
3560
|
+
throw new Error(
|
|
3561
|
+
`Invalid project ID "${projectId}". Project IDs must not contain path separators or traversal segments.`
|
|
3562
|
+
);
|
|
3497
3563
|
}
|
|
3498
3564
|
}
|
|
3499
3565
|
function resolveProjectLockPath(runtimeRoot, projectId) {
|
|
3500
3566
|
const store = new OrchestratorFsStore(runtimeRoot);
|
|
3501
|
-
const projectsRoot = resolve2(runtimeRoot, "projects");
|
|
3502
3567
|
const projectDir = resolve2(store.projectDir(projectId));
|
|
3503
|
-
const
|
|
3504
|
-
|
|
3505
|
-
|
|
3568
|
+
const resolvedRuntimeRoot = resolve2(runtimeRoot);
|
|
3569
|
+
const relativeProjectDir = relative2(resolvedRuntimeRoot, projectDir);
|
|
3570
|
+
if (relativeProjectDir.startsWith("..") || isAbsolute2(relativeProjectDir)) {
|
|
3571
|
+
throw new Error(
|
|
3572
|
+
`Invalid project ID "${projectId}". Project lock path must stay within "${resolvedRuntimeRoot}".`
|
|
3573
|
+
);
|
|
3506
3574
|
}
|
|
3507
3575
|
return join4(projectDir, ".lock");
|
|
3508
3576
|
}
|
|
@@ -3530,15 +3598,24 @@ function isProcessRunning(pid) {
|
|
|
3530
3598
|
}
|
|
3531
3599
|
}
|
|
3532
3600
|
function isAlreadyExistsError2(error) {
|
|
3533
|
-
return Boolean(
|
|
3601
|
+
return Boolean(
|
|
3602
|
+
error && typeof error === "object" && "code" in error && error.code === "EEXIST"
|
|
3603
|
+
);
|
|
3534
3604
|
}
|
|
3535
3605
|
function isMissingFileError2(error) {
|
|
3536
|
-
return Boolean(
|
|
3606
|
+
return Boolean(
|
|
3607
|
+
error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")
|
|
3608
|
+
);
|
|
3537
3609
|
}
|
|
3538
3610
|
|
|
3539
|
-
// ../orchestrator/
|
|
3611
|
+
// ../orchestrator/src/index.ts
|
|
3540
3612
|
import { pathToFileURL } from "url";
|
|
3541
3613
|
import { resolve as resolve3 } from "path";
|
|
3614
|
+
|
|
3615
|
+
// ../orchestrator/src/runtime-factory.ts
|
|
3616
|
+
import { join as join5 } from "path";
|
|
3617
|
+
|
|
3618
|
+
// ../orchestrator/src/index.ts
|
|
3542
3619
|
function resolveOrchestratorLogLevel(value) {
|
|
3543
3620
|
if (!value || value === "normal") {
|
|
3544
3621
|
return "normal";
|
|
@@ -3546,7 +3623,9 @@ function resolveOrchestratorLogLevel(value) {
|
|
|
3546
3623
|
if (value === "verbose") {
|
|
3547
3624
|
return "verbose";
|
|
3548
3625
|
}
|
|
3549
|
-
throw new Error(
|
|
3626
|
+
throw new Error(
|
|
3627
|
+
`Unsupported log level: ${value}. Supported values: normal, verbose.`
|
|
3628
|
+
);
|
|
3550
3629
|
}
|
|
3551
3630
|
async function runCli(argv, dependencies = {}) {
|
|
3552
3631
|
const [command = "run-once", ...args] = argv;
|
|
@@ -3556,8 +3635,12 @@ async function runCli(argv, dependencies = {}) {
|
|
|
3556
3635
|
}
|
|
3557
3636
|
const runtimeRoot = resolve3(parsed.runtimeRoot ?? ".runtime");
|
|
3558
3637
|
const stderr = dependencies.stderr ?? process.stderr;
|
|
3559
|
-
const eventsDir = resolveOptionalPath(
|
|
3560
|
-
|
|
3638
|
+
const eventsDir = resolveOptionalPath(
|
|
3639
|
+
parsed.eventsDir ?? process.env.SYMPHONY_EVENTS_DIR
|
|
3640
|
+
);
|
|
3641
|
+
const logLevel = resolveOrchestratorLogLevel(
|
|
3642
|
+
parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL
|
|
3643
|
+
);
|
|
3561
3644
|
const service = await dependencies.createService?.(runtimeRoot, parsed.projectId, {
|
|
3562
3645
|
eventsDir,
|
|
3563
3646
|
logLevel,
|
|
@@ -3605,8 +3688,10 @@ async function runCli(argv, dependencies = {}) {
|
|
|
3605
3688
|
let exitCode = 0;
|
|
3606
3689
|
void cleanup().catch((error) => {
|
|
3607
3690
|
exitCode = 1;
|
|
3608
|
-
stderr.write(
|
|
3609
|
-
`)
|
|
3691
|
+
stderr.write(
|
|
3692
|
+
`Failed to shut down orchestrator after ${signal}: ${error instanceof Error ? error.message : String(error)}
|
|
3693
|
+
`
|
|
3694
|
+
);
|
|
3610
3695
|
}).finally(() => {
|
|
3611
3696
|
exitProcess(exitCode);
|
|
3612
3697
|
});
|
|
@@ -3733,13 +3818,19 @@ function resolveOptionalPath(value) {
|
|
|
3733
3818
|
}
|
|
3734
3819
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
3735
3820
|
main().catch((error) => {
|
|
3736
|
-
process.stderr.write(
|
|
3737
|
-
|
|
3821
|
+
process.stderr.write(
|
|
3822
|
+
`${error instanceof Error ? error.message : "Unknown error"}
|
|
3823
|
+
`
|
|
3824
|
+
);
|
|
3738
3825
|
process.exitCode = 1;
|
|
3739
3826
|
});
|
|
3740
3827
|
}
|
|
3741
3828
|
|
|
3742
3829
|
export {
|
|
3830
|
+
resolveTrackerAdapter2 as resolveTrackerAdapter,
|
|
3831
|
+
explainIssueDispatch,
|
|
3832
|
+
isActiveRunRecordStatus,
|
|
3833
|
+
parseIssueIdentifier,
|
|
3743
3834
|
OrchestratorService,
|
|
3744
3835
|
createStore,
|
|
3745
3836
|
acquireProjectLock,
|