@gh-symphony/cli 0.0.21 → 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 +36 -0
- package/dist/{chunk-SXGT7LOF.js → chunk-2TSM3INR.js} +26 -1
- package/dist/{chunk-A67CMOYE.js → chunk-2UW7NQLX.js} +1 -1
- package/dist/{chunk-MVRF7BES.js → chunk-36KYEDEO.js} +10 -1
- package/dist/{chunk-C7G7RJ4G.js → chunk-DDL4BWSL.js} +1 -1
- package/dist/{chunk-XN5ABWZ6.js → chunk-DFLXHNYQ.js} +26 -30
- package/dist/{chunk-KY6WKH66.js → chunk-E7HYEEZD.js} +70 -52
- package/dist/{chunk-QEONJ5DZ.js → chunk-EEQQWTXS.js} +1288 -92
- package/dist/chunk-GDE6FYN4.js +26 -0
- package/dist/{chunk-Y6TYJMNT.js → chunk-GSX2FV3M.js} +10 -16
- package/dist/{chunk-JN3TQVFV.js → chunk-HMLBBZNY.js} +11 -2
- package/dist/{chunk-5NV3LSAJ.js → chunk-IWFX2FMA.js} +5 -1
- package/dist/{chunk-MYVJ6HK4.js → chunk-PUDXVBSN.js} +706 -376
- package/dist/{chunk-ROGRTUFI.js → chunk-QIRE2VXS.js} +14 -3
- package/dist/{chunk-S6VIK4FF.js → chunk-ZHOKYUO3.js} +337 -13
- package/dist/{config-cmd-DNXNL26Z.js → config-cmd-Z3A7V6NC.js} +1 -1
- package/dist/{doctor-4HBRICHP.js → doctor-EJUMPBMW.js} +4 -4
- package/dist/index.js +88 -21
- package/dist/{init-HZ3JEDGQ.js → init-54HMKNYI.js} +3 -3
- package/dist/{logs-6JKKYDGJ.js → logs-GTZ4U5JE.js} +2 -2
- package/dist/project-RMYMZSFV.js +25 -0
- package/dist/{recover-L3MJHHDA.js → recover-LTLKMTRX.js} +7 -7
- package/dist/repo-WI7GF6XQ.js +749 -0
- package/dist/{run-XJQ6BF7U.js → run-IHN3ZL35.js} +21 -9
- package/dist/{setup-B2SVLW2R.js → setup-TZJSM3QV.js} +14 -13
- package/dist/start-RTAHQMR2.js +19 -0
- package/dist/status-F4D52OVK.js +12 -0
- package/dist/stop-MDKMJPVR.js +10 -0
- package/dist/{upgrade-OJXPZRYE.js → upgrade-O33S2SJK.js} +2 -2
- package/dist/{version-TBDCTKDO.js → version-CW54Q7BK.js} +1 -1
- package/dist/worker-entry.js +369 -13
- package/dist/{workflow-BLJH2HC3.js → workflow-L3KT6HB7.js} +5 -5
- package/package.json +3 -3
- package/dist/project-25NQ4J4Y.js +0 -24
- package/dist/repo-TDCWQR6P.js +0 -379
- package/dist/start-I2CC7BLW.js +0 -18
- package/dist/status-QSCFVGRQ.js +0 -11
- package/dist/stop-7MFCBQVW.js +0 -9
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
resolveTrackerAdapter
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2TSM3INR.js";
|
|
5
5
|
import {
|
|
6
6
|
DEFAULT_MAX_FAILURE_RETRIES,
|
|
7
7
|
DEFAULT_WORKFLOW_LIFECYCLE,
|
|
@@ -31,13 +31,457 @@ import {
|
|
|
31
31
|
resolveWorkflowRuntimeTimeouts,
|
|
32
32
|
safeReadDir,
|
|
33
33
|
scheduleRetryAt
|
|
34
|
-
} 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
|
+
}
|
|
35
479
|
|
|
36
480
|
// ../orchestrator/src/service.ts
|
|
37
481
|
import { mkdir as mkdir3, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
38
482
|
import { createWriteStream, mkdirSync } from "fs";
|
|
39
483
|
import { spawn as spawn2 } from "child_process";
|
|
40
|
-
import { join as join3 } from "path";
|
|
484
|
+
import { isAbsolute, join as join3 } from "path";
|
|
41
485
|
import { StringDecoder } from "string_decoder";
|
|
42
486
|
import { fileURLToPath } from "url";
|
|
43
487
|
|
|
@@ -47,7 +491,7 @@ import { randomUUID } from "crypto";
|
|
|
47
491
|
import {
|
|
48
492
|
access,
|
|
49
493
|
mkdir,
|
|
50
|
-
readFile,
|
|
494
|
+
readFile as readFile2,
|
|
51
495
|
rename,
|
|
52
496
|
rm,
|
|
53
497
|
stat,
|
|
@@ -118,10 +562,13 @@ async function syncRepositoryForRun(input) {
|
|
|
118
562
|
});
|
|
119
563
|
}
|
|
120
564
|
async function ensureIssueWorkspaceRepository(input) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
565
|
+
if (!input.existingWorkspace) {
|
|
566
|
+
return cloneRepositoryForRun({
|
|
567
|
+
repository: input.repository,
|
|
568
|
+
targetDirectory: input.issueWorkspacePath
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return syncExistingIssueWorkspaceRepository(input);
|
|
125
572
|
}
|
|
126
573
|
async function loadRepositoryWorkflow(repositoryDirectory, _repository) {
|
|
127
574
|
const workflowPath = join(repositoryDirectory, "WORKFLOW.md");
|
|
@@ -160,16 +607,117 @@ function runCommand(command, args) {
|
|
|
160
607
|
});
|
|
161
608
|
});
|
|
162
609
|
}
|
|
163
|
-
async function readGitHead(repositoryDirectory) {
|
|
610
|
+
async function readGitHead(repositoryDirectory) {
|
|
611
|
+
try {
|
|
612
|
+
return await runCommandCapture("git", [
|
|
613
|
+
"-C",
|
|
614
|
+
repositoryDirectory,
|
|
615
|
+
"rev-parse",
|
|
616
|
+
"HEAD"
|
|
617
|
+
]);
|
|
618
|
+
} catch {
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
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) {
|
|
164
716
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
repositoryDirectory,
|
|
168
|
-
"rev-parse",
|
|
169
|
-
"HEAD"
|
|
170
|
-
]);
|
|
717
|
+
await access(path, constants.F_OK);
|
|
718
|
+
return true;
|
|
171
719
|
} catch {
|
|
172
|
-
return
|
|
720
|
+
return false;
|
|
173
721
|
}
|
|
174
722
|
}
|
|
175
723
|
function runCommandCapture(command, args) {
|
|
@@ -271,7 +819,7 @@ function isAlreadyExistsError(error) {
|
|
|
271
819
|
}
|
|
272
820
|
async function readLockOwner(lockDirectory) {
|
|
273
821
|
await access(join(lockDirectory, "owner"), constants.R_OK);
|
|
274
|
-
const owner = await
|
|
822
|
+
const owner = await readFile2(join(lockDirectory, "owner"), "utf8");
|
|
275
823
|
return owner.split("\n", 1)[0] || null;
|
|
276
824
|
}
|
|
277
825
|
function wait(ms) {
|
|
@@ -304,20 +852,14 @@ var OrchestratorFsStore = class {
|
|
|
304
852
|
}
|
|
305
853
|
resolvedRuntimeRoot;
|
|
306
854
|
resolvedEventsMirrorRoot;
|
|
307
|
-
|
|
308
|
-
return
|
|
309
|
-
}
|
|
310
|
-
projectDir(projectId) {
|
|
311
|
-
return join2(this.projectsRoot(), projectId);
|
|
855
|
+
projectDir(_projectId) {
|
|
856
|
+
return this.runtimeRoot;
|
|
312
857
|
}
|
|
313
|
-
|
|
314
|
-
return join2(this.
|
|
858
|
+
runsDir() {
|
|
859
|
+
return join2(this.runtimeRoot, "runs");
|
|
315
860
|
}
|
|
316
|
-
runDir(runId,
|
|
317
|
-
|
|
318
|
-
return join2(this.runtimeRoot, "projects", "__unknown__", "runs", runId);
|
|
319
|
-
}
|
|
320
|
-
return join2(this.projectRunsDir(projectId), runId);
|
|
861
|
+
runDir(runId, _projectId) {
|
|
862
|
+
return join2(this.runsDir(), runId);
|
|
321
863
|
}
|
|
322
864
|
async loadProjectConfig(projectId) {
|
|
323
865
|
return readJsonFile(
|
|
@@ -370,7 +912,7 @@ var OrchestratorFsStore = class {
|
|
|
370
912
|
}
|
|
371
913
|
async saveProjectStatus(status) {
|
|
372
914
|
await writeJsonFile(
|
|
373
|
-
join2(this.projectDir(
|
|
915
|
+
join2(this.projectDir(), "status.json"),
|
|
374
916
|
status
|
|
375
917
|
);
|
|
376
918
|
}
|
|
@@ -389,16 +931,12 @@ var OrchestratorFsStore = class {
|
|
|
389
931
|
) ?? null;
|
|
390
932
|
}
|
|
391
933
|
async loadAllRuns() {
|
|
392
|
-
const
|
|
393
|
-
const runDirectories = await Promise.all(
|
|
394
|
-
projectIds.map(async (projectId) => {
|
|
395
|
-
const entries = await safeReadDir(this.projectRunsDir(projectId));
|
|
396
|
-
return entries.map((entry) => this.runDir(entry, projectId));
|
|
397
|
-
})
|
|
398
|
-
);
|
|
934
|
+
const runIds = await safeReadDir(this.runsDir());
|
|
399
935
|
const runs = await Promise.all(
|
|
400
|
-
|
|
401
|
-
(
|
|
936
|
+
runIds.map(
|
|
937
|
+
(runId) => readJsonFile(
|
|
938
|
+
join2(this.runDir(runId), "run.json")
|
|
939
|
+
)
|
|
402
940
|
)
|
|
403
941
|
);
|
|
404
942
|
return runs.filter((run) => Boolean(run));
|
|
@@ -483,7 +1021,7 @@ var OrchestratorFsStore = class {
|
|
|
483
1021
|
}
|
|
484
1022
|
}
|
|
485
1023
|
issueWorkspaceDir(projectId, workspaceKey) {
|
|
486
|
-
return join2(this.
|
|
1024
|
+
return join2(this.runtimeRoot, workspaceKey);
|
|
487
1025
|
}
|
|
488
1026
|
async loadIssueWorkspace(projectId, workspaceKey) {
|
|
489
1027
|
return await readJsonFile(
|
|
@@ -491,15 +1029,29 @@ var OrchestratorFsStore = class {
|
|
|
491
1029
|
) ?? null;
|
|
492
1030
|
}
|
|
493
1031
|
async loadIssueWorkspaces(projectId) {
|
|
494
|
-
const
|
|
495
|
-
const entries = await safeReadDir(issuesDir);
|
|
1032
|
+
const entries = await safeReadDir(this.runtimeRoot);
|
|
496
1033
|
const records = await Promise.all(
|
|
497
|
-
entries.map((entry) =>
|
|
1034
|
+
entries.map(async (entry) => {
|
|
1035
|
+
if (!await this.isIssueWorkspaceEntry(entry)) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
return this.loadIssueWorkspace(projectId, entry);
|
|
1039
|
+
})
|
|
498
1040
|
);
|
|
499
1041
|
return records.filter(
|
|
500
1042
|
(record) => Boolean(record)
|
|
501
1043
|
);
|
|
502
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;
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
return (await stat2(join2(this.runtimeRoot, entry))).isDirectory();
|
|
1051
|
+
} catch {
|
|
1052
|
+
return false;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
503
1055
|
async saveIssueWorkspace(record) {
|
|
504
1056
|
await writeJsonFile(
|
|
505
1057
|
join2(
|
|
@@ -514,15 +1066,12 @@ var OrchestratorFsStore = class {
|
|
|
514
1066
|
await rm2(dir, { recursive: true, force: true });
|
|
515
1067
|
}
|
|
516
1068
|
async findRunDir(runId) {
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (run || await pathExists(join2(candidate, "events.ndjson"))) {
|
|
524
|
-
return candidate;
|
|
525
|
-
}
|
|
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;
|
|
526
1075
|
}
|
|
527
1076
|
return null;
|
|
528
1077
|
}
|
|
@@ -544,7 +1093,7 @@ async function writeJsonFile(path, value) {
|
|
|
544
1093
|
await writeFile2(temporaryPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
545
1094
|
await rename2(temporaryPath, path);
|
|
546
1095
|
}
|
|
547
|
-
async function
|
|
1096
|
+
async function pathExists2(path) {
|
|
548
1097
|
try {
|
|
549
1098
|
await stat2(path);
|
|
550
1099
|
return true;
|
|
@@ -556,120 +1105,6 @@ async function pathExists(path) {
|
|
|
556
1105
|
}
|
|
557
1106
|
}
|
|
558
1107
|
|
|
559
|
-
// ../tracker-file/src/file-tracker-adapter.ts
|
|
560
|
-
import { readFile as readFile2 } from "fs/promises";
|
|
561
|
-
function requireTrackerSetting(project, key) {
|
|
562
|
-
const value = project.tracker.settings?.[key];
|
|
563
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
564
|
-
throw new Error(
|
|
565
|
-
`Tracker adapter "file" requires the "${key}" setting.`
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
return value;
|
|
569
|
-
}
|
|
570
|
-
function parseIssueNumber(identifier) {
|
|
571
|
-
const match = identifier.match(/#(\d+)$/);
|
|
572
|
-
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
573
|
-
}
|
|
574
|
-
function isValidIssueShape(entry) {
|
|
575
|
-
if (!entry || typeof entry !== "object") return false;
|
|
576
|
-
const e = entry;
|
|
577
|
-
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";
|
|
578
|
-
}
|
|
579
|
-
var fileTrackerAdapter = {
|
|
580
|
-
async listIssues(project) {
|
|
581
|
-
const issuesPath = requireTrackerSetting(project, "issuesPath");
|
|
582
|
-
try {
|
|
583
|
-
const raw = await readFile2(issuesPath, "utf-8");
|
|
584
|
-
const parsed = JSON.parse(raw);
|
|
585
|
-
if (!Array.isArray(parsed)) {
|
|
586
|
-
throw new Error(
|
|
587
|
-
`Expected an array of issues in ${issuesPath}, got ${typeof parsed}`
|
|
588
|
-
);
|
|
589
|
-
}
|
|
590
|
-
const valid = [];
|
|
591
|
-
for (let i = 0; i < parsed.length; i++) {
|
|
592
|
-
if (isValidIssueShape(parsed[i])) {
|
|
593
|
-
valid.push(parsed[i]);
|
|
594
|
-
} else {
|
|
595
|
-
process.stderr.write(
|
|
596
|
-
`[tracker-file] Skipping invalid issue at index ${i} in ${issuesPath}
|
|
597
|
-
`
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
return valid;
|
|
602
|
-
} catch (err) {
|
|
603
|
-
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
604
|
-
return [];
|
|
605
|
-
}
|
|
606
|
-
if (err instanceof SyntaxError) {
|
|
607
|
-
return [];
|
|
608
|
-
}
|
|
609
|
-
throw err;
|
|
610
|
-
}
|
|
611
|
-
},
|
|
612
|
-
async listIssuesByStates(project, states) {
|
|
613
|
-
if (states.length === 0) {
|
|
614
|
-
return [];
|
|
615
|
-
}
|
|
616
|
-
const issues = await this.listIssues(project);
|
|
617
|
-
const normalizedStates = new Set(
|
|
618
|
-
states.map((state) => state.trim().toLowerCase())
|
|
619
|
-
);
|
|
620
|
-
return issues.filter(
|
|
621
|
-
(issue) => normalizedStates.has(issue.state.trim().toLowerCase())
|
|
622
|
-
);
|
|
623
|
-
},
|
|
624
|
-
async fetchIssueStatesByIds(project, issueIds) {
|
|
625
|
-
if (issueIds.length === 0) {
|
|
626
|
-
return [];
|
|
627
|
-
}
|
|
628
|
-
const issues = await this.listIssues(project);
|
|
629
|
-
const ids = new Set(issueIds);
|
|
630
|
-
return issues.filter((issue) => ids.has(issue.id));
|
|
631
|
-
},
|
|
632
|
-
buildWorkerEnvironment(_project, _issue) {
|
|
633
|
-
return {
|
|
634
|
-
SYMPHONY_FILE_TRACKER: "true"
|
|
635
|
-
};
|
|
636
|
-
},
|
|
637
|
-
reviveIssue(project, run) {
|
|
638
|
-
return {
|
|
639
|
-
id: run.issueId,
|
|
640
|
-
identifier: run.issueIdentifier,
|
|
641
|
-
number: parseIssueNumber(run.issueIdentifier),
|
|
642
|
-
title: run.issueTitle ?? run.issueIdentifier,
|
|
643
|
-
description: null,
|
|
644
|
-
priority: null,
|
|
645
|
-
state: run.issueState,
|
|
646
|
-
branchName: null,
|
|
647
|
-
url: null,
|
|
648
|
-
labels: [],
|
|
649
|
-
blockedBy: [],
|
|
650
|
-
createdAt: null,
|
|
651
|
-
updatedAt: null,
|
|
652
|
-
repository: run.repository,
|
|
653
|
-
tracker: {
|
|
654
|
-
adapter: "file",
|
|
655
|
-
bindingId: project.tracker.bindingId,
|
|
656
|
-
itemId: run.issueId
|
|
657
|
-
},
|
|
658
|
-
metadata: {}
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
|
|
663
|
-
// ../orchestrator/src/tracker-adapters.ts
|
|
664
|
-
var localAdapters = /* @__PURE__ */ new Map([
|
|
665
|
-
["file", fileTrackerAdapter]
|
|
666
|
-
]);
|
|
667
|
-
function resolveTrackerAdapter2(tracker) {
|
|
668
|
-
const local = localAdapters.get(tracker.adapter);
|
|
669
|
-
if (local) return local;
|
|
670
|
-
return resolveTrackerAdapter(tracker);
|
|
671
|
-
}
|
|
672
|
-
|
|
673
1108
|
// ../orchestrator/src/service.ts
|
|
674
1109
|
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
675
1110
|
var DEFAULT_CONCURRENCY = 3;
|
|
@@ -678,12 +1113,12 @@ var CONTINUATION_RETRY_DELAY_MS = 1e3;
|
|
|
678
1113
|
var DEFAULT_WORKER_COMMAND = "node packages/worker/dist/index.js";
|
|
679
1114
|
var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
|
|
680
1115
|
var LOW_RATE_LIMIT_WARNING_THRESHOLD = 0.05;
|
|
681
|
-
var
|
|
1116
|
+
var MAX_FAILURE_RETRIES_EXCEEDED_REASON2 = "max_failure_retries_exceeded";
|
|
682
1117
|
var STUCK_WORKER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
683
1118
|
function isUsableWorkflowResolution(resolution) {
|
|
684
1119
|
return resolution.isValid || resolution.usedLastKnownGood;
|
|
685
1120
|
}
|
|
686
|
-
function
|
|
1121
|
+
function parseTimestampMs2(value) {
|
|
687
1122
|
if (!value) {
|
|
688
1123
|
return null;
|
|
689
1124
|
}
|
|
@@ -771,7 +1206,6 @@ var OrchestratorService = class {
|
|
|
771
1206
|
) : null;
|
|
772
1207
|
const currentRun = isMatchingIssueRun(
|
|
773
1208
|
currentRunCandidate,
|
|
774
|
-
this.projectConfig.projectId,
|
|
775
1209
|
issueRecord.issueId,
|
|
776
1210
|
issueIdentifier
|
|
777
1211
|
) ? currentRunCandidate : await this.findLatestRunForIssue(issueRecord.issueId, issueIdentifier);
|
|
@@ -902,7 +1336,7 @@ var OrchestratorService = class {
|
|
|
902
1336
|
const allRuns = (await this.store.loadAllRuns()).filter(
|
|
903
1337
|
(run) => run.projectId === tenant.projectId
|
|
904
1338
|
);
|
|
905
|
-
const activeRuns = allRuns.filter((run) =>
|
|
1339
|
+
const activeRuns = allRuns.filter((run) => isActiveRunRecordStatus(run.status));
|
|
906
1340
|
for (const run of activeRuns) {
|
|
907
1341
|
const outcome = await this.reconcileRun(
|
|
908
1342
|
tenant,
|
|
@@ -916,7 +1350,7 @@ var OrchestratorService = class {
|
|
|
916
1350
|
}
|
|
917
1351
|
}
|
|
918
1352
|
const reconciledRuns = (await this.store.loadAllRuns()).filter(
|
|
919
|
-
(run) => run.projectId === tenant.projectId &&
|
|
1353
|
+
(run) => run.projectId === tenant.projectId && isActiveRunRecordStatus(run.status)
|
|
920
1354
|
);
|
|
921
1355
|
const projectRunsAfterReconcile = (await this.store.loadAllRuns()).filter(
|
|
922
1356
|
(run) => run.projectId === tenant.projectId
|
|
@@ -925,7 +1359,7 @@ var OrchestratorService = class {
|
|
|
925
1359
|
try {
|
|
926
1360
|
pollIntervalMs = await this.loadProjectPollInterval(tenant);
|
|
927
1361
|
const currentActiveRuns = (await this.store.loadAllRuns()).filter(
|
|
928
|
-
(run) => run.projectId === tenant.projectId &&
|
|
1362
|
+
(run) => run.projectId === tenant.projectId && isActiveRunRecordStatus(run.status)
|
|
929
1363
|
);
|
|
930
1364
|
const {
|
|
931
1365
|
runs: syncedActiveRuns,
|
|
@@ -976,14 +1410,14 @@ var OrchestratorService = class {
|
|
|
976
1410
|
);
|
|
977
1411
|
const concurrency = await this.getProjectConcurrency(tenant);
|
|
978
1412
|
const currentlyActive = issueRecords.filter(
|
|
979
|
-
(record) =>
|
|
1413
|
+
(record) => isIssueOrchestrationClaimedState(record.state)
|
|
980
1414
|
).length;
|
|
981
1415
|
const availableSlots = Math.max(0, concurrency - currentlyActive);
|
|
982
1416
|
const latestRunsByIssueId = buildLatestRunMapByIssueId(
|
|
983
1417
|
projectRunsAfterReconcile
|
|
984
1418
|
);
|
|
985
1419
|
const unscheduledCandidates = actionableCandidates.filter((issue) => {
|
|
986
|
-
if (
|
|
1420
|
+
if (hasConvergenceLockedRunForIssue(
|
|
987
1421
|
projectRunsAfterReconcile,
|
|
988
1422
|
issue.id,
|
|
989
1423
|
issue.state,
|
|
@@ -992,7 +1426,7 @@ var OrchestratorService = class {
|
|
|
992
1426
|
return false;
|
|
993
1427
|
}
|
|
994
1428
|
return !issueRecords.some(
|
|
995
|
-
(record) => record.issueId === issue.id &&
|
|
1429
|
+
(record) => record.issueId === issue.id && isIssueOrchestrationClaimedState(record.state)
|
|
996
1430
|
);
|
|
997
1431
|
});
|
|
998
1432
|
const sortedCandidates = sortCandidatesForDispatch(unscheduledCandidates);
|
|
@@ -1026,7 +1460,6 @@ var OrchestratorService = class {
|
|
|
1026
1460
|
}
|
|
1027
1461
|
const preferredWorkspaceKey = deriveIssueWorkspaceKey(
|
|
1028
1462
|
{
|
|
1029
|
-
projectId: tenant.projectId,
|
|
1030
1463
|
adapter: issue.tracker.adapter,
|
|
1031
1464
|
issueSubjectId: issue.id
|
|
1032
1465
|
},
|
|
@@ -1078,7 +1511,7 @@ var OrchestratorService = class {
|
|
|
1078
1511
|
);
|
|
1079
1512
|
}
|
|
1080
1513
|
for (const issueRecord of issueRecords) {
|
|
1081
|
-
if (!
|
|
1514
|
+
if (!isIssueOrchestrationClaimedState(issueRecord.state)) {
|
|
1082
1515
|
continue;
|
|
1083
1516
|
}
|
|
1084
1517
|
const issue = trackedIssuesByIdentifier.get(issueRecord.identifier);
|
|
@@ -1089,7 +1522,6 @@ var OrchestratorService = class {
|
|
|
1089
1522
|
const activeRun = syncedActiveRuns.find(
|
|
1090
1523
|
(run) => isMatchingIssueRun(
|
|
1091
1524
|
run,
|
|
1092
|
-
tenant.projectId,
|
|
1093
1525
|
issueRecord.issueId,
|
|
1094
1526
|
issueRecord.identifier
|
|
1095
1527
|
)
|
|
@@ -1159,7 +1591,7 @@ var OrchestratorService = class {
|
|
|
1159
1591
|
(run) => run.projectId === tenant.projectId
|
|
1160
1592
|
);
|
|
1161
1593
|
const latestRuns = allTenantRuns.filter(
|
|
1162
|
-
(run) =>
|
|
1594
|
+
(run) => isActiveRunRecordStatus(run.status)
|
|
1163
1595
|
);
|
|
1164
1596
|
rateLimits = rateLimits ?? resolveProjectRateLimits(latestRuns, []);
|
|
1165
1597
|
const status = buildProjectSnapshot({
|
|
@@ -1215,7 +1647,6 @@ var OrchestratorService = class {
|
|
|
1215
1647
|
try {
|
|
1216
1648
|
const resolution = await this.loadStartupCleanupWorkflow(
|
|
1217
1649
|
tenant,
|
|
1218
|
-
issue.repository,
|
|
1219
1650
|
workflowCache
|
|
1220
1651
|
);
|
|
1221
1652
|
if (!resolution.isValid) {
|
|
@@ -1256,32 +1687,22 @@ var OrchestratorService = class {
|
|
|
1256
1687
|
}
|
|
1257
1688
|
return String(error);
|
|
1258
1689
|
}
|
|
1259
|
-
async resolveStartupCleanupTerminalStates(tenant,
|
|
1690
|
+
async resolveStartupCleanupTerminalStates(tenant, _workspaceRecords, workflowCache) {
|
|
1260
1691
|
const terminalStates = /* @__PURE__ */ new Map();
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
);
|
|
1273
|
-
} catch {
|
|
1274
|
-
continue;
|
|
1275
|
-
}
|
|
1276
|
-
if (!isUsableWorkflowResolution(resolution)) {
|
|
1277
|
-
continue;
|
|
1278
|
-
}
|
|
1279
|
-
for (const state of resolution.lifecycle.terminalStates) {
|
|
1280
|
-
const normalizedState = state.trim().toLowerCase();
|
|
1281
|
-
if (!terminalStates.has(normalizedState)) {
|
|
1282
|
-
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
|
+
}
|
|
1283
1703
|
}
|
|
1284
1704
|
}
|
|
1705
|
+
} catch {
|
|
1285
1706
|
}
|
|
1286
1707
|
if (terminalStates.size === 0) {
|
|
1287
1708
|
for (const state of DEFAULT_WORKFLOW_LIFECYCLE.terminalStates) {
|
|
@@ -1290,59 +1711,16 @@ var OrchestratorService = class {
|
|
|
1290
1711
|
}
|
|
1291
1712
|
return [...terminalStates.values()];
|
|
1292
1713
|
}
|
|
1293
|
-
|
|
1294
|
-
const
|
|
1295
|
-
for (const repository of tenant.repositories) {
|
|
1296
|
-
repositories.set(
|
|
1297
|
-
this.startupCleanupRepositoryKey(repository.owner, repository.name),
|
|
1298
|
-
repository
|
|
1299
|
-
);
|
|
1300
|
-
}
|
|
1301
|
-
for (const workspaceRecord of workspaceRecords) {
|
|
1302
|
-
const repository = this.parseWorkspaceRepositoryRef(workspaceRecord);
|
|
1303
|
-
if (!repository) {
|
|
1304
|
-
continue;
|
|
1305
|
-
}
|
|
1306
|
-
const key = this.startupCleanupRepositoryKey(
|
|
1307
|
-
repository.owner,
|
|
1308
|
-
repository.name
|
|
1309
|
-
);
|
|
1310
|
-
if (!repositories.has(key)) {
|
|
1311
|
-
repositories.set(key, repository);
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
return [...repositories.values()];
|
|
1315
|
-
}
|
|
1316
|
-
parseWorkspaceRepositoryRef(workspaceRecord) {
|
|
1317
|
-
const match = workspaceRecord.issueIdentifier.match(
|
|
1318
|
-
/^([^/]+)\/([^#]+)#\d+$/
|
|
1319
|
-
);
|
|
1320
|
-
if (!match) {
|
|
1321
|
-
return null;
|
|
1322
|
-
}
|
|
1323
|
-
const owner = match[1];
|
|
1324
|
-
const name = match[2];
|
|
1325
|
-
if (!owner || !name) {
|
|
1326
|
-
return null;
|
|
1327
|
-
}
|
|
1328
|
-
return {
|
|
1329
|
-
owner,
|
|
1330
|
-
name,
|
|
1331
|
-
cloneUrl: workspaceRecord.repositoryPath
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
startupCleanupRepositoryKey(owner, name) {
|
|
1335
|
-
return `${owner}/${name}`;
|
|
1336
|
-
}
|
|
1337
|
-
async loadStartupCleanupWorkflow(tenant, repository, workflowCache) {
|
|
1338
|
-
const cacheKey = this.workflowCacheKey(repository);
|
|
1714
|
+
async loadStartupCleanupWorkflow(tenant, workflowCache) {
|
|
1715
|
+
const cacheKey = this.workflowCacheKey(tenant.repository);
|
|
1339
1716
|
const cachedResolution = workflowCache.get(cacheKey);
|
|
1340
1717
|
if (cachedResolution) {
|
|
1341
1718
|
return cachedResolution;
|
|
1342
1719
|
}
|
|
1343
|
-
const resolutionPromise =
|
|
1344
|
-
|
|
1345
|
-
|
|
1720
|
+
const resolutionPromise = this.loadProjectWorkflow(
|
|
1721
|
+
tenant,
|
|
1722
|
+
tenant.repository
|
|
1723
|
+
);
|
|
1346
1724
|
workflowCache.set(cacheKey, resolutionPromise);
|
|
1347
1725
|
return resolutionPromise;
|
|
1348
1726
|
}
|
|
@@ -1409,10 +1787,10 @@ var OrchestratorService = class {
|
|
|
1409
1787
|
}
|
|
1410
1788
|
candidates.push(issue);
|
|
1411
1789
|
}
|
|
1412
|
-
if (!lifecycle
|
|
1790
|
+
if (!lifecycle) {
|
|
1413
1791
|
const resolution = await this.loadProjectWorkflow(
|
|
1414
1792
|
tenant,
|
|
1415
|
-
tenant.
|
|
1793
|
+
tenant.repository
|
|
1416
1794
|
);
|
|
1417
1795
|
if (isUsableWorkflowResolution(resolution)) {
|
|
1418
1796
|
lifecycle = resolution.lifecycle;
|
|
@@ -1429,26 +1807,7 @@ var OrchestratorService = class {
|
|
|
1429
1807
|
};
|
|
1430
1808
|
}
|
|
1431
1809
|
isIssueCandidateEligible(issue, lifecycle, issues) {
|
|
1432
|
-
|
|
1433
|
-
return false;
|
|
1434
|
-
}
|
|
1435
|
-
if (!matchesWorkflowState(issue.state, lifecycle.blockerCheckStates) || issue.blockedBy.length === 0) {
|
|
1436
|
-
return true;
|
|
1437
|
-
}
|
|
1438
|
-
return !issue.blockedBy.some((blockerRef) => {
|
|
1439
|
-
if (blockerRef.state && isStateTerminal(blockerRef.state, lifecycle)) {
|
|
1440
|
-
return false;
|
|
1441
|
-
}
|
|
1442
|
-
if (blockerRef.identifier) {
|
|
1443
|
-
const blockerIssue = issues.find(
|
|
1444
|
-
(candidate) => candidate.identifier === blockerRef.identifier
|
|
1445
|
-
);
|
|
1446
|
-
if (blockerIssue?.state) {
|
|
1447
|
-
return !isStateTerminal(blockerIssue.state, lifecycle);
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
return true;
|
|
1451
|
-
});
|
|
1810
|
+
return isIssueCandidateEligibleWithReason(issue, lifecycle, issues).eligible;
|
|
1452
1811
|
}
|
|
1453
1812
|
async loadProjectWorkflow(tenant, repository) {
|
|
1454
1813
|
const cacheKey = this.workflowCacheKey(repository);
|
|
@@ -1474,10 +1833,7 @@ var OrchestratorService = class {
|
|
|
1474
1833
|
repository.owner,
|
|
1475
1834
|
repository.name
|
|
1476
1835
|
);
|
|
1477
|
-
const
|
|
1478
|
-
repository,
|
|
1479
|
-
targetDirectory: cacheRoot
|
|
1480
|
-
});
|
|
1836
|
+
const repositoryDirectory = this.resolveWorkflowRepositoryDirectory(repository);
|
|
1481
1837
|
const resolution = await loadRepositoryWorkflow(
|
|
1482
1838
|
repositoryDirectory,
|
|
1483
1839
|
repository
|
|
@@ -1486,7 +1842,7 @@ var OrchestratorService = class {
|
|
|
1486
1842
|
repository,
|
|
1487
1843
|
cacheRoot,
|
|
1488
1844
|
resolution,
|
|
1489
|
-
|
|
1845
|
+
true
|
|
1490
1846
|
);
|
|
1491
1847
|
}
|
|
1492
1848
|
async startRun(tenant, issue) {
|
|
@@ -1502,7 +1858,6 @@ var OrchestratorService = class {
|
|
|
1502
1858
|
const workspaceRuntimeDir = runDir;
|
|
1503
1859
|
const issueSubjectId = issue.id;
|
|
1504
1860
|
const identity = {
|
|
1505
|
-
projectId: tenant.projectId,
|
|
1506
1861
|
adapter: issue.tracker.adapter,
|
|
1507
1862
|
issueSubjectId
|
|
1508
1863
|
};
|
|
@@ -1510,7 +1865,10 @@ var OrchestratorService = class {
|
|
|
1510
1865
|
identity,
|
|
1511
1866
|
issue.identifier
|
|
1512
1867
|
);
|
|
1513
|
-
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(
|
|
1868
|
+
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(
|
|
1869
|
+
identity,
|
|
1870
|
+
tenant.projectId
|
|
1871
|
+
);
|
|
1514
1872
|
const existingWorkspaceRecord = await this.store.loadIssueWorkspace(
|
|
1515
1873
|
tenant.projectId,
|
|
1516
1874
|
preferredWorkspaceKey
|
|
@@ -1526,7 +1884,8 @@ var OrchestratorService = class {
|
|
|
1526
1884
|
);
|
|
1527
1885
|
const repositoryDirectory = await ensureIssueWorkspaceRepository({
|
|
1528
1886
|
repository: issue.repository,
|
|
1529
|
-
issueWorkspacePath
|
|
1887
|
+
issueWorkspacePath,
|
|
1888
|
+
existingWorkspace: Boolean(existingWorkspaceRecord)
|
|
1530
1889
|
});
|
|
1531
1890
|
if (!existingWorkspaceRecord) {
|
|
1532
1891
|
const workspaceRecord = {
|
|
@@ -1672,12 +2031,8 @@ var OrchestratorService = class {
|
|
|
1672
2031
|
SYMPHONY_CUMULATIVE_TOTAL_TOKENS: "0",
|
|
1673
2032
|
SYMPHONY_LAST_TURN_SUMMARY: "",
|
|
1674
2033
|
SYMPHONY_SESSION_STARTED_AT: "",
|
|
1675
|
-
SYMPHONY_READ_TIMEOUT_MS: String(
|
|
1676
|
-
|
|
1677
|
-
),
|
|
1678
|
-
SYMPHONY_TURN_TIMEOUT_MS: String(
|
|
1679
|
-
runtimeTimeouts.turnTimeoutMs
|
|
1680
|
-
)
|
|
2034
|
+
SYMPHONY_READ_TIMEOUT_MS: String(runtimeTimeouts.readTimeoutMs),
|
|
2035
|
+
SYMPHONY_TURN_TIMEOUT_MS: String(runtimeTimeouts.turnTimeoutMs)
|
|
1681
2036
|
}),
|
|
1682
2037
|
detached: true,
|
|
1683
2038
|
stdio: ["ignore", "ignore", "pipe"]
|
|
@@ -1850,10 +2205,10 @@ var OrchestratorService = class {
|
|
|
1850
2205
|
if (run.processId && this.isProcessRunning(run.processId)) {
|
|
1851
2206
|
const retryPolicy = await this.loadRetryPolicy(tenant, run.repository);
|
|
1852
2207
|
const configuredStallTimeoutMs = retryPolicy?.stallTimeoutMs ?? null;
|
|
1853
|
-
const lastActivityAtMs =
|
|
2208
|
+
const lastActivityAtMs = parseTimestampMs2(
|
|
1854
2209
|
run.lastEventAt ?? run.startedAt
|
|
1855
2210
|
);
|
|
1856
|
-
const startedAtMs =
|
|
2211
|
+
const startedAtMs = parseTimestampMs2(run.startedAt);
|
|
1857
2212
|
const elapsedSinceLastActivityMs = lastActivityAtMs === null ? null : now.getTime() - lastActivityAtMs;
|
|
1858
2213
|
const runningSinceMs = startedAtMs === null ? null : now.getTime() - startedAtMs;
|
|
1859
2214
|
const isStalledByWorkflowTimeout = configuredStallTimeoutMs !== null && configuredStallTimeoutMs > 0 && elapsedSinceLastActivityMs !== null && elapsedSinceLastActivityMs > configuredStallTimeoutMs;
|
|
@@ -1885,7 +2240,6 @@ var OrchestratorService = class {
|
|
|
1885
2240
|
identifier: run.issueIdentifier,
|
|
1886
2241
|
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
1887
2242
|
{
|
|
1888
|
-
projectId: tenant.projectId,
|
|
1889
2243
|
adapter: tenant.tracker.adapter,
|
|
1890
2244
|
issueSubjectId: run.issueSubjectId
|
|
1891
2245
|
},
|
|
@@ -2020,7 +2374,7 @@ var OrchestratorService = class {
|
|
|
2020
2374
|
);
|
|
2021
2375
|
if (retryKind === "failure" && failureRetryCount >= maxFailureRetries) {
|
|
2022
2376
|
const lastError = [
|
|
2023
|
-
`Run suppressed: ${
|
|
2377
|
+
`Run suppressed: ${MAX_FAILURE_RETRIES_EXCEEDED_REASON2}.`,
|
|
2024
2378
|
`failureRetryCount=${failureRetryCount}.`,
|
|
2025
2379
|
`maxFailureRetries=${maxFailureRetries}.`
|
|
2026
2380
|
].join(" ");
|
|
@@ -2042,7 +2396,7 @@ var OrchestratorService = class {
|
|
|
2042
2396
|
projectId: run.projectId,
|
|
2043
2397
|
issueIdentifier: run.issueIdentifier,
|
|
2044
2398
|
issueId: run.issueId,
|
|
2045
|
-
reason:
|
|
2399
|
+
reason: MAX_FAILURE_RETRIES_EXCEEDED_REASON2
|
|
2046
2400
|
});
|
|
2047
2401
|
this.logVerbose(
|
|
2048
2402
|
`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`
|
|
@@ -2053,7 +2407,6 @@ var OrchestratorService = class {
|
|
|
2053
2407
|
identifier: run.issueIdentifier,
|
|
2054
2408
|
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2055
2409
|
{
|
|
2056
|
-
projectId: tenant.projectId,
|
|
2057
2410
|
adapter: tenant.tracker.adapter,
|
|
2058
2411
|
issueSubjectId: run.issueSubjectId
|
|
2059
2412
|
},
|
|
@@ -2104,7 +2457,6 @@ var OrchestratorService = class {
|
|
|
2104
2457
|
identifier: run.issueIdentifier,
|
|
2105
2458
|
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2106
2459
|
{
|
|
2107
|
-
projectId: tenant.projectId,
|
|
2108
2460
|
adapter: tenant.tracker.adapter,
|
|
2109
2461
|
issueSubjectId: run.issueSubjectId
|
|
2110
2462
|
},
|
|
@@ -2556,7 +2908,6 @@ var OrchestratorService = class {
|
|
|
2556
2908
|
identifier: recoveredRecord.issueIdentifier,
|
|
2557
2909
|
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey(
|
|
2558
2910
|
{
|
|
2559
|
-
projectId: tenant.projectId,
|
|
2560
2911
|
adapter: tenant.tracker.adapter,
|
|
2561
2912
|
issueSubjectId: recoveredRecord.issueSubjectId
|
|
2562
2913
|
},
|
|
@@ -2591,37 +2942,25 @@ var OrchestratorService = class {
|
|
|
2591
2942
|
};
|
|
2592
2943
|
}
|
|
2593
2944
|
async loadProjectPollInterval(tenant) {
|
|
2594
|
-
const
|
|
2595
|
-
tenant
|
|
2596
|
-
|
|
2597
|
-
return isUsableWorkflowResolution(resolution) ? resolution.workflow.polling.intervalMs : NaN;
|
|
2598
|
-
})
|
|
2599
|
-
);
|
|
2600
|
-
const validIntervals = intervals.filter(
|
|
2601
|
-
(value) => Number.isFinite(value) && value > 0
|
|
2945
|
+
const resolution = await this.loadProjectWorkflow(
|
|
2946
|
+
tenant,
|
|
2947
|
+
tenant.repository
|
|
2602
2948
|
);
|
|
2603
|
-
|
|
2949
|
+
const interval = isUsableWorkflowResolution(resolution) ? resolution.workflow.polling.intervalMs : NaN;
|
|
2950
|
+
return Number.isFinite(interval) && interval > 0 ? interval : DEFAULT_POLL_INTERVAL_MS;
|
|
2604
2951
|
}
|
|
2605
2952
|
async loadProjectMaxConcurrentByState(tenant) {
|
|
2606
2953
|
const result = {};
|
|
2607
|
-
const
|
|
2608
|
-
tenant
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
)
|
|
2616
|
-
|
|
2617
|
-
if (!resolution) continue;
|
|
2618
|
-
if (!isUsableWorkflowResolution(resolution)) continue;
|
|
2619
|
-
const stateLimits = resolution.workflow.agent.maxConcurrentAgentsByState;
|
|
2620
|
-
for (const [state, limit] of Object.entries(stateLimits)) {
|
|
2621
|
-
const existing = result[state];
|
|
2622
|
-
const numLimit = typeof limit === "number" ? limit : Number(limit);
|
|
2623
|
-
result[state] = existing === void 0 ? numLimit : Math.min(existing, numLimit);
|
|
2624
|
-
}
|
|
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);
|
|
2625
2964
|
}
|
|
2626
2965
|
return result;
|
|
2627
2966
|
}
|
|
@@ -2651,23 +2990,10 @@ var OrchestratorService = class {
|
|
|
2651
2990
|
if (this.dependencies.concurrency !== void 0) {
|
|
2652
2991
|
return this.dependencies.concurrency;
|
|
2653
2992
|
}
|
|
2654
|
-
const
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
project,
|
|
2659
|
-
repository
|
|
2660
|
-
);
|
|
2661
|
-
return isUsableWorkflowResolution(resolution) ? resolution.workflow.agent.maxConcurrentAgents : NaN;
|
|
2662
|
-
} catch {
|
|
2663
|
-
return NaN;
|
|
2664
|
-
}
|
|
2665
|
-
})
|
|
2666
|
-
);
|
|
2667
|
-
const validLimits = limits.filter(
|
|
2668
|
-
(value) => Number.isFinite(value) && value >= 0
|
|
2669
|
-
);
|
|
2670
|
-
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;
|
|
2671
2997
|
}
|
|
2672
2998
|
async resolveWorkflowResolution(repository, cacheRoot, resolution, changed) {
|
|
2673
2999
|
const cacheKey = this.workflowCacheKey(repository);
|
|
@@ -2731,6 +3057,26 @@ var OrchestratorService = class {
|
|
|
2731
3057
|
workflowCacheKey(repository) {
|
|
2732
3058
|
return `${repository.owner}/${repository.name}:${this.normalizeRepositoryCloneUrl(repository.cloneUrl)}`;
|
|
2733
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
|
+
}
|
|
2734
3080
|
normalizeRepositoryCloneUrl(cloneUrl) {
|
|
2735
3081
|
if (cloneUrl.startsWith("file://")) {
|
|
2736
3082
|
try {
|
|
@@ -2787,7 +3133,6 @@ var OrchestratorService = class {
|
|
|
2787
3133
|
async cleanupTerminalIssueWorkspace(tenant, issue, now, workflowResolution) {
|
|
2788
3134
|
const issueSubjectId = issue.id;
|
|
2789
3135
|
const identity = {
|
|
2790
|
-
projectId: tenant.projectId,
|
|
2791
3136
|
adapter: issue.tracker.adapter,
|
|
2792
3137
|
issueSubjectId
|
|
2793
3138
|
};
|
|
@@ -2795,7 +3140,10 @@ var OrchestratorService = class {
|
|
|
2795
3140
|
identity,
|
|
2796
3141
|
issue.identifier
|
|
2797
3142
|
);
|
|
2798
|
-
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(
|
|
3143
|
+
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(
|
|
3144
|
+
identity,
|
|
3145
|
+
tenant.projectId
|
|
3146
|
+
);
|
|
2799
3147
|
const orchestrationRecord = (await this.store.loadProjectIssueOrchestrations(tenant.projectId)).find((record) => record.issueId === issue.id);
|
|
2800
3148
|
const workspaceRecord = (orchestrationRecord ? await this.store.loadIssueWorkspace(
|
|
2801
3149
|
tenant.projectId,
|
|
@@ -2866,11 +3214,11 @@ var OrchestratorService = class {
|
|
|
2866
3214
|
if (issueRecord.failureRetryCount < maxFailureRetries) {
|
|
2867
3215
|
return false;
|
|
2868
3216
|
}
|
|
2869
|
-
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)) {
|
|
2870
3218
|
return false;
|
|
2871
3219
|
}
|
|
2872
|
-
const issueUpdatedAtMs =
|
|
2873
|
-
const suppressedAtMs =
|
|
3220
|
+
const issueUpdatedAtMs = parseTimestampMs2(issue.updatedAt);
|
|
3221
|
+
const suppressedAtMs = parseTimestampMs2(
|
|
2874
3222
|
latestRun.completedAt ?? latestRun.updatedAt
|
|
2875
3223
|
);
|
|
2876
3224
|
if (issueUpdatedAtMs === null || suppressedAtMs === null) {
|
|
@@ -2902,7 +3250,7 @@ function resolveProjectRateLimits(runs, issues) {
|
|
|
2902
3250
|
if (!isRecord(run.rateLimits)) {
|
|
2903
3251
|
continue;
|
|
2904
3252
|
}
|
|
2905
|
-
const timestamp =
|
|
3253
|
+
const timestamp = parseTimestampMs2(
|
|
2906
3254
|
run.lastEventAt ?? run.updatedAt ?? run.startedAt
|
|
2907
3255
|
);
|
|
2908
3256
|
const sortableTimestamp = timestamp ?? -Infinity;
|
|
@@ -2982,22 +3330,6 @@ function buildRuntimeSession(existing, sessionId, threadId, status, startedAt, u
|
|
|
2982
3330
|
function resolvePersistedCumulativeTurnCount(run) {
|
|
2983
3331
|
return run.cumulativeTurnCount ?? run.turnCount ?? 0;
|
|
2984
3332
|
}
|
|
2985
|
-
function hasConvergenceLockedRun(runs, issueId, issueState, issueUpdatedAt) {
|
|
2986
|
-
const latestRun = runs.filter((run) => run.issueId === issueId).sort(
|
|
2987
|
-
(left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime()
|
|
2988
|
-
)[0];
|
|
2989
|
-
if (latestRun?.runtimeSession?.exitClassification !== "convergence-detected" || latestRun.issueState !== issueState) {
|
|
2990
|
-
return false;
|
|
2991
|
-
}
|
|
2992
|
-
const convergedAtMs = parseTimestampMs(
|
|
2993
|
-
latestRun.completedAt ?? latestRun.updatedAt
|
|
2994
|
-
);
|
|
2995
|
-
const issueUpdatedAtMs = parseTimestampMs(issueUpdatedAt);
|
|
2996
|
-
if (convergedAtMs === null || issueUpdatedAtMs === null) {
|
|
2997
|
-
return true;
|
|
2998
|
-
}
|
|
2999
|
-
return issueUpdatedAtMs <= convergedAtMs;
|
|
3000
|
-
}
|
|
3001
3333
|
function resolveCumulativeTurnCount(run, turnCount) {
|
|
3002
3334
|
const carriedTotal = resolvePersistedCumulativeTurnCount(run);
|
|
3003
3335
|
if (turnCount === null) {
|
|
@@ -3103,17 +3435,14 @@ function buildLatestRunMapByIssueId(runs) {
|
|
|
3103
3435
|
latestRuns.set(run.issueId, run);
|
|
3104
3436
|
continue;
|
|
3105
3437
|
}
|
|
3106
|
-
const runUpdatedAtMs =
|
|
3107
|
-
const existingUpdatedAtMs =
|
|
3438
|
+
const runUpdatedAtMs = parseTimestampMs2(run.updatedAt) ?? -Infinity;
|
|
3439
|
+
const existingUpdatedAtMs = parseTimestampMs2(existing.updatedAt) ?? -Infinity;
|
|
3108
3440
|
if (runUpdatedAtMs > existingUpdatedAtMs) {
|
|
3109
3441
|
latestRuns.set(run.issueId, run);
|
|
3110
3442
|
}
|
|
3111
3443
|
}
|
|
3112
3444
|
return latestRuns;
|
|
3113
3445
|
}
|
|
3114
|
-
function isIssueOrchestrationClaimed(state) {
|
|
3115
|
-
return state === "claimed" || state === "running" || state === "retry_queued";
|
|
3116
|
-
}
|
|
3117
3446
|
function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
3118
3447
|
const existingRecord = issueRecords.find((record) => record.issueId === nextRecord.issueId) ?? null;
|
|
3119
3448
|
const remaining = issueRecords.filter(
|
|
@@ -3139,14 +3468,11 @@ function releaseIssueOrchestration(issueRecords, issueId, now) {
|
|
|
3139
3468
|
} : record
|
|
3140
3469
|
);
|
|
3141
3470
|
}
|
|
3142
|
-
function isActiveRunStatus(status) {
|
|
3143
|
-
return status === "pending" || status === "starting" || status === "running" || status === "retrying";
|
|
3144
|
-
}
|
|
3145
3471
|
|
|
3146
3472
|
// ../orchestrator/src/lock.ts
|
|
3147
3473
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3148
3474
|
import { mkdir as mkdir4, open as open2, readFile as readFile4, rm as rm4 } from "fs/promises";
|
|
3149
|
-
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";
|
|
3150
3476
|
import { setTimeout as delay } from "timers/promises";
|
|
3151
3477
|
var LOCK_READ_RETRY_DELAY_MS = 10;
|
|
3152
3478
|
var LOCK_READ_RETRY_LIMIT = 20;
|
|
@@ -3238,12 +3564,12 @@ function assertValidProjectId(projectId) {
|
|
|
3238
3564
|
}
|
|
3239
3565
|
function resolveProjectLockPath(runtimeRoot, projectId) {
|
|
3240
3566
|
const store = new OrchestratorFsStore(runtimeRoot);
|
|
3241
|
-
const projectsRoot = resolve2(runtimeRoot, "projects");
|
|
3242
3567
|
const projectDir = resolve2(store.projectDir(projectId));
|
|
3243
|
-
const
|
|
3244
|
-
|
|
3568
|
+
const resolvedRuntimeRoot = resolve2(runtimeRoot);
|
|
3569
|
+
const relativeProjectDir = relative2(resolvedRuntimeRoot, projectDir);
|
|
3570
|
+
if (relativeProjectDir.startsWith("..") || isAbsolute2(relativeProjectDir)) {
|
|
3245
3571
|
throw new Error(
|
|
3246
|
-
`Invalid project ID "${projectId}". Project lock path must stay within "${
|
|
3572
|
+
`Invalid project ID "${projectId}". Project lock path must stay within "${resolvedRuntimeRoot}".`
|
|
3247
3573
|
);
|
|
3248
3574
|
}
|
|
3249
3575
|
return join4(projectDir, ".lock");
|
|
@@ -3501,6 +3827,10 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
|
|
|
3501
3827
|
}
|
|
3502
3828
|
|
|
3503
3829
|
export {
|
|
3830
|
+
resolveTrackerAdapter2 as resolveTrackerAdapter,
|
|
3831
|
+
explainIssueDispatch,
|
|
3832
|
+
isActiveRunRecordStatus,
|
|
3833
|
+
parseIssueIdentifier,
|
|
3504
3834
|
OrchestratorService,
|
|
3505
3835
|
createStore,
|
|
3506
3836
|
acquireProjectLock,
|