@gh-symphony/cli 0.0.14 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-5NV3LSAJ.js +11 -0
- package/dist/chunk-6HBZC3BE.js +468 -0
- package/dist/chunk-76QPITKI.js +109 -0
- package/dist/chunk-EFMFGOWM.js +3575 -0
- package/dist/chunk-IWR4UQEJ.js +2250 -0
- package/dist/chunk-JO3AXHQI.js +130 -0
- package/dist/chunk-MHIWAIVD.js +876 -0
- package/dist/chunk-MVRF7BES.js +68 -0
- package/dist/chunk-ROGRTUFI.js +108 -0
- package/dist/chunk-TF3QNWNC.js +1121 -0
- package/dist/chunk-TH5QPO3Y.js +67 -0
- package/dist/config-cmd-AZ7POMAA.js +110 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +568 -356
- package/dist/init-EZXQAXZM.js +17 -0
- package/dist/logs-6LNGT2GF.js +188 -0
- package/dist/project-557FE2GD.js +679 -0
- package/dist/recover-LVBI2TGH.js +131 -0
- package/dist/repo-R3XBIVAX.js +121 -0
- package/dist/run-WITYAYFZ.js +108 -0
- package/dist/start-JUFKNL3N.js +16 -0
- package/dist/status-3WK5BWRZ.js +11 -0
- package/dist/stop-AA3AP5M6.js +9 -0
- package/dist/version-VBB62JWI.js +30 -0
- package/dist/worker-entry.js +1828 -0
- package/package.json +9 -4
- package/dist/ansi.d.ts +0 -15
- package/dist/ansi.js +0 -53
- package/dist/commands/config-cmd.d.ts +0 -3
- package/dist/commands/config-cmd.js +0 -90
- package/dist/commands/help.d.ts +0 -3
- package/dist/commands/help.js +0 -55
- package/dist/commands/init.d.ts +0 -34
- package/dist/commands/init.js +0 -477
- package/dist/commands/logs.d.ts +0 -3
- package/dist/commands/logs.js +0 -184
- package/dist/commands/project.d.ts +0 -3
- package/dist/commands/project.js +0 -649
- package/dist/commands/recover.d.ts +0 -3
- package/dist/commands/recover.js +0 -119
- package/dist/commands/repo.d.ts +0 -3
- package/dist/commands/repo.js +0 -103
- package/dist/commands/run.d.ts +0 -3
- package/dist/commands/run.js +0 -95
- package/dist/commands/start.d.ts +0 -20
- package/dist/commands/start.js +0 -344
- package/dist/commands/status-refresh.d.ts +0 -9
- package/dist/commands/status-refresh.js +0 -27
- package/dist/commands/status.d.ts +0 -3
- package/dist/commands/status.js +0 -237
- package/dist/commands/stop.d.ts +0 -3
- package/dist/commands/stop.js +0 -92
- package/dist/commands/version.d.ts +0 -3
- package/dist/commands/version.js +0 -21
- package/dist/completion.d.ts +0 -1
- package/dist/completion.js +0 -204
- package/dist/config.d.ts +0 -38
- package/dist/config.js +0 -82
- package/dist/context/context-types.d.ts +0 -36
- package/dist/context/context-types.js +0 -1
- package/dist/context/generate-context-yaml.d.ts +0 -15
- package/dist/context/generate-context-yaml.js +0 -129
- package/dist/dashboard/renderer.d.ts +0 -9
- package/dist/dashboard/renderer.js +0 -220
- package/dist/detection/environment-detector.d.ts +0 -11
- package/dist/detection/environment-detector.js +0 -140
- package/dist/github/client.d.ts +0 -71
- package/dist/github/client.js +0 -348
- package/dist/github/gh-auth.d.ts +0 -34
- package/dist/github/gh-auth.js +0 -110
- package/dist/mapping/smart-defaults.d.ts +0 -17
- package/dist/mapping/smart-defaults.js +0 -86
- package/dist/orchestrator-runtime.d.ts +0 -1
- package/dist/orchestrator-runtime.js +0 -4
- package/dist/orchestrator-status-endpoint.d.ts +0 -5
- package/dist/orchestrator-status-endpoint.js +0 -27
- package/dist/project-selection.d.ts +0 -8
- package/dist/project-selection.js +0 -56
- package/dist/skills/skill-writer.d.ts +0 -14
- package/dist/skills/skill-writer.js +0 -62
- package/dist/skills/templates/commit.d.ts +0 -2
- package/dist/skills/templates/commit.js +0 -45
- package/dist/skills/templates/document.d.ts +0 -7
- package/dist/skills/templates/document.js +0 -16
- package/dist/skills/templates/gh-project.d.ts +0 -2
- package/dist/skills/templates/gh-project.js +0 -88
- package/dist/skills/templates/gh-symphony.d.ts +0 -2
- package/dist/skills/templates/gh-symphony.js +0 -125
- package/dist/skills/templates/index.d.ts +0 -8
- package/dist/skills/templates/index.js +0 -28
- package/dist/skills/templates/land.d.ts +0 -2
- package/dist/skills/templates/land.js +0 -59
- package/dist/skills/templates/pull.d.ts +0 -2
- package/dist/skills/templates/pull.js +0 -41
- package/dist/skills/templates/push.d.ts +0 -2
- package/dist/skills/templates/push.js +0 -36
- package/dist/skills/types.d.ts +0 -23
- package/dist/skills/types.js +0 -1
- package/dist/workflow/generate-reference-workflow.d.ts +0 -9
- package/dist/workflow/generate-reference-workflow.js +0 -261
- package/dist/workflow/generate-workflow-md.d.ts +0 -12
- package/dist/workflow/generate-workflow-md.js +0 -134
|
@@ -0,0 +1,3575 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_WORKFLOW_LIFECYCLE,
|
|
4
|
+
WorkflowConfigStore,
|
|
5
|
+
buildHookEnv,
|
|
6
|
+
buildProjectSnapshot,
|
|
7
|
+
buildPromptVariables,
|
|
8
|
+
createDefaultWorkflowResolution,
|
|
9
|
+
createInvalidWorkflowResolution,
|
|
10
|
+
deriveIssueWorkspaceKey,
|
|
11
|
+
deriveIssueWorkspaceKeyFromIdentifier,
|
|
12
|
+
deriveLegacyIssueWorkspaceKey,
|
|
13
|
+
executeWorkspaceHook,
|
|
14
|
+
isFileMissing,
|
|
15
|
+
isMatchingIssueRun,
|
|
16
|
+
isOrchestratorChannelEvent,
|
|
17
|
+
isStateActive,
|
|
18
|
+
isStateTerminal,
|
|
19
|
+
mapIssueOrchestrationStateToStatus,
|
|
20
|
+
matchesWorkflowState,
|
|
21
|
+
parseRecentEvents,
|
|
22
|
+
readEnvFile,
|
|
23
|
+
readJsonFile,
|
|
24
|
+
renderPrompt,
|
|
25
|
+
resolveIssueWorkspaceDirectory,
|
|
26
|
+
safeReadDir,
|
|
27
|
+
scheduleRetryAt
|
|
28
|
+
} from "./chunk-TF3QNWNC.js";
|
|
29
|
+
|
|
30
|
+
// ../orchestrator/dist/service.js
|
|
31
|
+
import { mkdir as mkdir3, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
32
|
+
import { createWriteStream, mkdirSync } from "fs";
|
|
33
|
+
import { spawn as spawn2 } from "child_process";
|
|
34
|
+
import { join as join3 } from "path";
|
|
35
|
+
import { StringDecoder } from "string_decoder";
|
|
36
|
+
import { fileURLToPath } from "url";
|
|
37
|
+
|
|
38
|
+
// ../orchestrator/dist/git.js
|
|
39
|
+
import { spawn } from "child_process";
|
|
40
|
+
import { randomUUID } from "crypto";
|
|
41
|
+
import { access, mkdir, readFile, rename, rm, stat, writeFile } from "fs/promises";
|
|
42
|
+
import { constants } from "fs";
|
|
43
|
+
import { join } from "path";
|
|
44
|
+
var workflowConfigStore = new WorkflowConfigStore();
|
|
45
|
+
var LOCK_RETRY_MS = 100;
|
|
46
|
+
var LOCK_STALE_MS = 30 * 60 * 1e3;
|
|
47
|
+
var LOCK_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
48
|
+
async function cloneRepositoryForRun(input) {
|
|
49
|
+
const result = await syncRepositoryForRun(input);
|
|
50
|
+
return result.repositoryDirectory;
|
|
51
|
+
}
|
|
52
|
+
async function syncRepositoryForRun(input) {
|
|
53
|
+
await mkdir(input.targetDirectory, { recursive: true });
|
|
54
|
+
const repositoryDirectory = join(input.targetDirectory, "repository");
|
|
55
|
+
const lockDirectory = join(input.targetDirectory, "repository.lock");
|
|
56
|
+
return withRepositoryLock(lockDirectory, async () => {
|
|
57
|
+
let hasGit = false;
|
|
58
|
+
try {
|
|
59
|
+
await access(join(repositoryDirectory, ".git"), constants.R_OK);
|
|
60
|
+
hasGit = true;
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
if (hasGit) {
|
|
64
|
+
try {
|
|
65
|
+
const beforeHead = await readGitHead(repositoryDirectory);
|
|
66
|
+
await runCommand("git", [
|
|
67
|
+
"-C",
|
|
68
|
+
repositoryDirectory,
|
|
69
|
+
"pull",
|
|
70
|
+
"--ff-only"
|
|
71
|
+
]);
|
|
72
|
+
const afterHead = await readGitHead(repositoryDirectory);
|
|
73
|
+
return {
|
|
74
|
+
repositoryDirectory,
|
|
75
|
+
changed: beforeHead !== afterHead
|
|
76
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
await rm(repositoryDirectory, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
await rm(repositoryDirectory, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
const tempRepositoryDirectory = join(input.targetDirectory, `repository.tmp-${process.pid}-${Date.now()}`);
|
|
84
|
+
await rm(tempRepositoryDirectory, { recursive: true, force: true });
|
|
85
|
+
try {
|
|
86
|
+
await runCommand("git", [
|
|
87
|
+
"clone",
|
|
88
|
+
"--depth",
|
|
89
|
+
"1",
|
|
90
|
+
input.repository.cloneUrl,
|
|
91
|
+
tempRepositoryDirectory
|
|
92
|
+
]);
|
|
93
|
+
await rename(tempRepositoryDirectory, repositoryDirectory);
|
|
94
|
+
return {
|
|
95
|
+
repositoryDirectory,
|
|
96
|
+
changed: true
|
|
97
|
+
};
|
|
98
|
+
} finally {
|
|
99
|
+
await rm(tempRepositoryDirectory, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
async function ensureIssueWorkspaceRepository(input) {
|
|
104
|
+
return cloneRepositoryForRun({
|
|
105
|
+
repository: input.repository,
|
|
106
|
+
targetDirectory: input.issueWorkspacePath
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
async function loadRepositoryWorkflow(repositoryDirectory, _repository) {
|
|
110
|
+
const workflowPath = join(repositoryDirectory, "WORKFLOW.md");
|
|
111
|
+
try {
|
|
112
|
+
return await workflowConfigStore.load(workflowPath);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (isMissingFileError(error)) {
|
|
115
|
+
return createDefaultWorkflowResolution();
|
|
116
|
+
}
|
|
117
|
+
return createInvalidWorkflowResolution(workflowPath, error instanceof Error ? error.message : "workflow_parse_error");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function runCommand(command, args) {
|
|
121
|
+
return new Promise((resolve4, reject) => {
|
|
122
|
+
const child = spawn(command, args, {
|
|
123
|
+
stdio: "pipe"
|
|
124
|
+
});
|
|
125
|
+
let stderr = "";
|
|
126
|
+
child.stderr?.on("data", (chunk) => {
|
|
127
|
+
stderr += String(chunk);
|
|
128
|
+
});
|
|
129
|
+
child.once("error", reject);
|
|
130
|
+
child.once("exit", (code) => {
|
|
131
|
+
if (code === 0) {
|
|
132
|
+
resolve4();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code ?? "unknown"}`));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async function readGitHead(repositoryDirectory) {
|
|
140
|
+
try {
|
|
141
|
+
return await runCommandCapture("git", [
|
|
142
|
+
"-C",
|
|
143
|
+
repositoryDirectory,
|
|
144
|
+
"rev-parse",
|
|
145
|
+
"HEAD"
|
|
146
|
+
]);
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function runCommandCapture(command, args) {
|
|
152
|
+
return new Promise((resolve4, reject) => {
|
|
153
|
+
const child = spawn(command, args, {
|
|
154
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
155
|
+
});
|
|
156
|
+
let stdout = "";
|
|
157
|
+
let stderr = "";
|
|
158
|
+
child.stdout?.on("data", (chunk) => {
|
|
159
|
+
stdout += String(chunk);
|
|
160
|
+
});
|
|
161
|
+
child.stderr?.on("data", (chunk) => {
|
|
162
|
+
stderr += String(chunk);
|
|
163
|
+
});
|
|
164
|
+
child.once("error", reject);
|
|
165
|
+
child.once("exit", (code) => {
|
|
166
|
+
if (code === 0) {
|
|
167
|
+
resolve4(stdout.trim());
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code ?? "unknown"}`));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
async function withRepositoryLock(lockDirectory, fn) {
|
|
175
|
+
const ownerToken = await acquireRepositoryLock(lockDirectory);
|
|
176
|
+
try {
|
|
177
|
+
return await fn();
|
|
178
|
+
} finally {
|
|
179
|
+
await releaseRepositoryLock(lockDirectory, ownerToken);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async function acquireRepositoryLock(lockDirectory) {
|
|
183
|
+
const startedAt = Date.now();
|
|
184
|
+
const ownerToken = `${process.pid}:${randomUUID()}`;
|
|
185
|
+
for (; ; ) {
|
|
186
|
+
try {
|
|
187
|
+
await mkdir(lockDirectory);
|
|
188
|
+
await writeFile(join(lockDirectory, "owner"), `${ownerToken}
|
|
189
|
+
${(/* @__PURE__ */ new Date()).toISOString()}
|
|
190
|
+
`, "utf8");
|
|
191
|
+
return ownerToken;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (!isAlreadyExistsError(error)) {
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const stale = await isStaleLock(lockDirectory);
|
|
198
|
+
if (stale) {
|
|
199
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
|
|
203
|
+
throw new Error(`Timed out waiting for repository cache lock: ${lockDirectory}`);
|
|
204
|
+
}
|
|
205
|
+
await wait(LOCK_RETRY_MS);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
async function releaseRepositoryLock(lockDirectory, ownerToken) {
|
|
209
|
+
try {
|
|
210
|
+
const owner = await readLockOwner(lockDirectory);
|
|
211
|
+
if (owner !== ownerToken) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (isMissingFileError(error)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
|
220
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
async function isStaleLock(lockDirectory) {
|
|
223
|
+
try {
|
|
224
|
+
const details = await stat(lockDirectory);
|
|
225
|
+
return Date.now() - details.mtimeMs >= LOCK_STALE_MS;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (isMissingFileError(error)) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function isAlreadyExistsError(error) {
|
|
234
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
|
235
|
+
}
|
|
236
|
+
async function readLockOwner(lockDirectory) {
|
|
237
|
+
await access(join(lockDirectory, "owner"), constants.R_OK);
|
|
238
|
+
const owner = await readFile(join(lockDirectory, "owner"), "utf8");
|
|
239
|
+
return owner.split("\n", 1)[0] || null;
|
|
240
|
+
}
|
|
241
|
+
function wait(ms) {
|
|
242
|
+
return new Promise((resolve4) => {
|
|
243
|
+
setTimeout(resolve4, ms);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function isMissingFileError(error) {
|
|
247
|
+
return Boolean(error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ../orchestrator/dist/fs-store.js
|
|
251
|
+
import { mkdir as mkdir2, open, rename as rename2, rm as rm2, stat as stat2, writeFile as writeFile2, appendFile } from "fs/promises";
|
|
252
|
+
import { dirname, join as join2, relative, resolve } from "path";
|
|
253
|
+
var OrchestratorFsStore = class {
|
|
254
|
+
runtimeRoot;
|
|
255
|
+
resolvedRuntimeRoot;
|
|
256
|
+
resolvedEventsMirrorRoot;
|
|
257
|
+
constructor(runtimeRoot, options = {}) {
|
|
258
|
+
this.runtimeRoot = runtimeRoot;
|
|
259
|
+
this.resolvedRuntimeRoot = resolve(runtimeRoot);
|
|
260
|
+
this.resolvedEventsMirrorRoot = options.eventsMirrorRoot ? resolve(options.eventsMirrorRoot) : null;
|
|
261
|
+
}
|
|
262
|
+
projectsRoot() {
|
|
263
|
+
return join2(this.runtimeRoot, "projects");
|
|
264
|
+
}
|
|
265
|
+
projectDir(projectId) {
|
|
266
|
+
return join2(this.projectsRoot(), projectId);
|
|
267
|
+
}
|
|
268
|
+
projectRunsDir(projectId) {
|
|
269
|
+
return join2(this.projectDir(projectId), "runs");
|
|
270
|
+
}
|
|
271
|
+
runDir(runId, projectId) {
|
|
272
|
+
if (!projectId) {
|
|
273
|
+
return join2(this.runtimeRoot, "projects", "__unknown__", "runs", runId);
|
|
274
|
+
}
|
|
275
|
+
return join2(this.projectRunsDir(projectId), runId);
|
|
276
|
+
}
|
|
277
|
+
async loadProjectConfig(projectId) {
|
|
278
|
+
return readJsonFile(join2(this.projectDir(projectId), "project.json"));
|
|
279
|
+
}
|
|
280
|
+
async saveProjectConfig(config) {
|
|
281
|
+
await writeJsonFile(join2(this.projectDir(config.projectId), "project.json"), config);
|
|
282
|
+
}
|
|
283
|
+
async loadProjectIssueOrchestrations(projectId) {
|
|
284
|
+
const issuesPath = join2(this.projectDir(projectId), "issues.json");
|
|
285
|
+
const issues = await readJsonFile(issuesPath);
|
|
286
|
+
if (issues) {
|
|
287
|
+
return issues.map((issue) => ({
|
|
288
|
+
...issue,
|
|
289
|
+
completedOnce: issue.completedOnce ?? false
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
const legacyLeases = await readJsonFile(join2(this.projectDir(projectId), "leases.json")) ?? [];
|
|
293
|
+
if (legacyLeases.length === 0) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
const migratedIssues = legacyLeases.map((lease) => ({
|
|
297
|
+
issueId: lease.issueId,
|
|
298
|
+
identifier: lease.issueIdentifier,
|
|
299
|
+
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(lease.issueIdentifier),
|
|
300
|
+
completedOnce: false,
|
|
301
|
+
state: lease.status === "active" ? "claimed" : "released",
|
|
302
|
+
currentRunId: lease.status === "active" ? lease.runId : null,
|
|
303
|
+
retryEntry: null,
|
|
304
|
+
updatedAt: lease.updatedAt
|
|
305
|
+
}));
|
|
306
|
+
await this.saveProjectIssueOrchestrations(projectId, migratedIssues);
|
|
307
|
+
return migratedIssues;
|
|
308
|
+
}
|
|
309
|
+
async saveProjectIssueOrchestrations(projectId, issues) {
|
|
310
|
+
await writeJsonFile(join2(this.projectDir(projectId), "issues.json"), issues);
|
|
311
|
+
}
|
|
312
|
+
async saveProjectStatus(status) {
|
|
313
|
+
await writeJsonFile(join2(this.projectDir(status.projectId), "status.json"), status);
|
|
314
|
+
}
|
|
315
|
+
async loadProjectStatus(projectId) {
|
|
316
|
+
return await readJsonFile(join2(this.projectDir(projectId), "status.json")) ?? null;
|
|
317
|
+
}
|
|
318
|
+
async loadRun(runId, projectId) {
|
|
319
|
+
const runDirectory = projectId !== void 0 ? this.runDir(runId, projectId) : await this.findRunDir(runId);
|
|
320
|
+
if (!runDirectory) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return await readJsonFile(join2(runDirectory, "run.json")) ?? null;
|
|
324
|
+
}
|
|
325
|
+
async loadAllRuns() {
|
|
326
|
+
const projectIds = await safeReadDir(this.projectsRoot());
|
|
327
|
+
const runDirectories = await Promise.all(projectIds.map(async (projectId) => {
|
|
328
|
+
const entries = await safeReadDir(this.projectRunsDir(projectId));
|
|
329
|
+
return entries.map((entry) => this.runDir(entry, projectId));
|
|
330
|
+
}));
|
|
331
|
+
const runs = await Promise.all(runDirectories.flat().map((directory) => readJsonFile(join2(directory, "run.json"))));
|
|
332
|
+
return runs.filter((run) => Boolean(run));
|
|
333
|
+
}
|
|
334
|
+
async saveRun(run) {
|
|
335
|
+
await writeJsonFile(join2(this.runDir(run.runId, run.projectId), "run.json"), run);
|
|
336
|
+
}
|
|
337
|
+
async appendRunEvent(runId, event) {
|
|
338
|
+
const resolvedProjectId = "projectId" in event && typeof event.projectId === "string" ? event.projectId : void 0;
|
|
339
|
+
const runDirectory = resolvedProjectId !== void 0 ? this.runDir(runId, resolvedProjectId) : await this.findRunDir(runId);
|
|
340
|
+
if (!runDirectory) {
|
|
341
|
+
throw new Error(`Unable to resolve run directory for event append: ${runId}`);
|
|
342
|
+
}
|
|
343
|
+
const path = join2(runDirectory, "events.ndjson");
|
|
344
|
+
const resolvedPath = resolve(path);
|
|
345
|
+
const serializedEvent = JSON.stringify(event) + "\n";
|
|
346
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
347
|
+
await appendFile(path, serializedEvent, {
|
|
348
|
+
encoding: "utf8",
|
|
349
|
+
mode: 420
|
|
350
|
+
});
|
|
351
|
+
const mirrorPath = this.resolveMirroredEventsPath(resolvedPath);
|
|
352
|
+
if (!mirrorPath) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
await mkdir2(dirname(mirrorPath), { recursive: true });
|
|
357
|
+
await appendFile(mirrorPath, serializedEvent, {
|
|
358
|
+
encoding: "utf8",
|
|
359
|
+
mode: 420
|
|
360
|
+
});
|
|
361
|
+
} catch (error) {
|
|
362
|
+
console.warn(`Failed to mirror orchestrator event log to ${mirrorPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async loadRecentRunEvents(runId, limit = 20, projectId) {
|
|
366
|
+
const runDirectory = projectId !== void 0 ? this.runDir(runId, projectId) : await this.findRunDir(runId);
|
|
367
|
+
if (!runDirectory) {
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
const path = join2(runDirectory, "events.ndjson");
|
|
371
|
+
try {
|
|
372
|
+
if (limit <= 0) {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
const handle = await open(path, "r");
|
|
376
|
+
try {
|
|
377
|
+
const stats = await handle.stat();
|
|
378
|
+
let position = stats.size;
|
|
379
|
+
let tail = Buffer.alloc(0);
|
|
380
|
+
while (position > 0) {
|
|
381
|
+
const readSize = Math.min(position, 4096);
|
|
382
|
+
position -= readSize;
|
|
383
|
+
const chunk = Buffer.allocUnsafe(readSize);
|
|
384
|
+
await handle.read(chunk, 0, readSize, position);
|
|
385
|
+
tail = Buffer.concat([chunk, tail]);
|
|
386
|
+
const events = parseRecentEvents(tail.toString("utf8"), limit, {
|
|
387
|
+
allowPartialFirstLine: position > 0
|
|
388
|
+
});
|
|
389
|
+
if (events.length >= limit) {
|
|
390
|
+
return events;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return parseRecentEvents(tail.toString("utf8"), limit, {
|
|
394
|
+
allowPartialFirstLine: false
|
|
395
|
+
});
|
|
396
|
+
} finally {
|
|
397
|
+
await handle.close();
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (isFileMissing(error)) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
issueWorkspaceDir(projectId, workspaceKey) {
|
|
407
|
+
return join2(this.projectDir(projectId), "issues", workspaceKey);
|
|
408
|
+
}
|
|
409
|
+
async loadIssueWorkspace(projectId, workspaceKey) {
|
|
410
|
+
return await readJsonFile(join2(this.issueWorkspaceDir(projectId, workspaceKey), "workspace.json")) ?? null;
|
|
411
|
+
}
|
|
412
|
+
async loadIssueWorkspaces(projectId) {
|
|
413
|
+
const issuesDir = join2(this.projectDir(projectId), "issues");
|
|
414
|
+
const entries = await safeReadDir(issuesDir);
|
|
415
|
+
const records = await Promise.all(entries.map((entry) => this.loadIssueWorkspace(projectId, entry)));
|
|
416
|
+
return records.filter((record) => Boolean(record));
|
|
417
|
+
}
|
|
418
|
+
async saveIssueWorkspace(record) {
|
|
419
|
+
await writeJsonFile(join2(this.issueWorkspaceDir(record.projectId, record.workspaceKey), "workspace.json"), record);
|
|
420
|
+
}
|
|
421
|
+
async removeIssueWorkspace(projectId, workspaceKey) {
|
|
422
|
+
const dir = this.issueWorkspaceDir(projectId, workspaceKey);
|
|
423
|
+
await rm2(dir, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
async findRunDir(runId) {
|
|
426
|
+
const projectIds = await safeReadDir(this.projectsRoot());
|
|
427
|
+
for (const projectId of projectIds) {
|
|
428
|
+
const candidate = this.runDir(runId, projectId);
|
|
429
|
+
const run = await readJsonFile(join2(candidate, "run.json"));
|
|
430
|
+
if (run || await pathExists(join2(candidate, "events.ndjson"))) {
|
|
431
|
+
return candidate;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
resolveMirroredEventsPath(primaryPath) {
|
|
437
|
+
if (!this.resolvedEventsMirrorRoot) {
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const relativePath = relative(this.resolvedRuntimeRoot, primaryPath);
|
|
441
|
+
if (relativePath.startsWith("..")) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const mirrorPath = join2(this.resolvedEventsMirrorRoot, relativePath);
|
|
445
|
+
return mirrorPath === primaryPath ? null : mirrorPath;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
async function writeJsonFile(path, value) {
|
|
449
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
450
|
+
const temporaryPath = `${path}.tmp`;
|
|
451
|
+
await writeFile2(temporaryPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
452
|
+
await rename2(temporaryPath, path);
|
|
453
|
+
}
|
|
454
|
+
async function pathExists(path) {
|
|
455
|
+
try {
|
|
456
|
+
await stat2(path);
|
|
457
|
+
return true;
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (isFileMissing(error)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
throw error;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ../tracker-github/dist/adapter.js
|
|
467
|
+
var DEFAULT_API_URL = "https://api.github.com/graphql";
|
|
468
|
+
var DEFAULT_PAGE_SIZE = 25;
|
|
469
|
+
var DEFAULT_NETWORK_TIMEOUT_MS = 3e4;
|
|
470
|
+
var GitHubTrackerError = class extends Error {
|
|
471
|
+
};
|
|
472
|
+
var GitHubTrackerHttpError = class extends GitHubTrackerError {
|
|
473
|
+
status;
|
|
474
|
+
details;
|
|
475
|
+
constructor(message, status, details) {
|
|
476
|
+
super(message);
|
|
477
|
+
this.status = status;
|
|
478
|
+
this.details = details;
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
var GitHubTrackerQueryError = class extends GitHubTrackerError {
|
|
482
|
+
};
|
|
483
|
+
function normalizeProjectItem(projectId, item, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, priority = {}, rateLimits = null) {
|
|
484
|
+
if (item.content?.__typename !== "Issue") {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
const fieldValues = extractFieldValues(item.fieldValues?.nodes ?? []);
|
|
488
|
+
const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
|
|
489
|
+
const repository = item.content.repository;
|
|
490
|
+
const blockedBy = (item.content.blockedBy?.nodes ?? []).flatMap((node) => node ? [
|
|
491
|
+
{
|
|
492
|
+
id: node.id,
|
|
493
|
+
identifier: `${node.repository.owner.login}/${node.repository.name}#${node.number}`,
|
|
494
|
+
state: normalizeBlockerState(node.state, lifecycle)
|
|
495
|
+
}
|
|
496
|
+
] : []);
|
|
497
|
+
return {
|
|
498
|
+
id: item.content.id,
|
|
499
|
+
identifier: `${repository.owner.login}/${repository.name}#${item.content.number}`,
|
|
500
|
+
number: item.content.number,
|
|
501
|
+
title: item.content.title,
|
|
502
|
+
description: item.content.body,
|
|
503
|
+
priority: resolvePriority(item, priority),
|
|
504
|
+
state,
|
|
505
|
+
branchName: null,
|
|
506
|
+
url: item.content.url,
|
|
507
|
+
labels: (item.content.labels?.nodes ?? []).flatMap((label) => label?.name ? [label.name.toLowerCase()] : []).sort(),
|
|
508
|
+
blockedBy,
|
|
509
|
+
createdAt: item.content.createdAt,
|
|
510
|
+
updatedAt: item.content.updatedAt ?? item.updatedAt,
|
|
511
|
+
repository: {
|
|
512
|
+
owner: repository.owner.login,
|
|
513
|
+
name: repository.name,
|
|
514
|
+
url: repository.url,
|
|
515
|
+
cloneUrl: deriveCloneUrl(repository.url)
|
|
516
|
+
},
|
|
517
|
+
tracker: {
|
|
518
|
+
adapter: "github-project",
|
|
519
|
+
bindingId: projectId,
|
|
520
|
+
itemId: item.id
|
|
521
|
+
},
|
|
522
|
+
metadata: fieldValues,
|
|
523
|
+
rateLimits
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async function fetchProjectIssues(config, fetchImpl = fetch) {
|
|
527
|
+
const issues = [];
|
|
528
|
+
let cursor = null;
|
|
529
|
+
const priorityOptionIds = config.priorityFieldName ? await fetchPriorityOptionOrder(config, config.priorityFieldName, fetchImpl) : void 0;
|
|
530
|
+
const currentUserLogin = config.assignedOnly ? await fetchCurrentUserLogin(config, fetchImpl) : null;
|
|
531
|
+
let excludedCount = 0;
|
|
532
|
+
let latestRateLimits = null;
|
|
533
|
+
do {
|
|
534
|
+
const pageResult = await fetchProjectItemsPage(config, cursor, fetchImpl);
|
|
535
|
+
const page = pageResult.page;
|
|
536
|
+
latestRateLimits = pageResult.rateLimits ?? latestRateLimits;
|
|
537
|
+
const pageIssues = (page.nodes ?? []).flatMap((item) => {
|
|
538
|
+
if (!item) {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
541
|
+
const normalized = normalizeProjectItem(config.projectId, item, config.lifecycle, {
|
|
542
|
+
fieldName: config.priorityFieldName,
|
|
543
|
+
optionIds: priorityOptionIds
|
|
544
|
+
}, latestRateLimits);
|
|
545
|
+
if (!normalized) {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
if (currentUserLogin && !isIssueAssignedToLogin(item, currentUserLogin)) {
|
|
549
|
+
excludedCount += 1;
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
return [normalized];
|
|
553
|
+
});
|
|
554
|
+
issues.push(...pageIssues);
|
|
555
|
+
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
556
|
+
} while (cursor);
|
|
557
|
+
if (currentUserLogin) {
|
|
558
|
+
emitAssignedOnlyFilterEvent({
|
|
559
|
+
projectId: config.projectId,
|
|
560
|
+
currentUserLogin,
|
|
561
|
+
includedCount: issues.length,
|
|
562
|
+
excludedCount
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
if (latestRateLimits) {
|
|
566
|
+
for (const issue of issues) {
|
|
567
|
+
issue.rateLimits = latestRateLimits;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return issues;
|
|
571
|
+
}
|
|
572
|
+
async function fetchIssueStatesByIds(config, issueIds, fetchImpl = fetch) {
|
|
573
|
+
if (issueIds.length === 0) {
|
|
574
|
+
return [];
|
|
575
|
+
}
|
|
576
|
+
const issues = [];
|
|
577
|
+
for (const issueIdBatch of chunkValues([...new Set(issueIds)], 100)) {
|
|
578
|
+
const result = await executeGraphQLQueryWithMetadata(config, ISSUE_STATES_BY_IDS_QUERY, {
|
|
579
|
+
issueIds: issueIdBatch
|
|
580
|
+
}, fetchImpl);
|
|
581
|
+
const data = result.data;
|
|
582
|
+
const rateLimits = result.rateLimits;
|
|
583
|
+
for (const node of data.nodes ?? []) {
|
|
584
|
+
const projectItem = await resolveIssueProjectItemForStateLookup(config, node, fetchImpl);
|
|
585
|
+
const normalized = normalizeIssueStateLookupNode(config.projectId, node, projectItem, config.lifecycle, rateLimits);
|
|
586
|
+
if (normalized) {
|
|
587
|
+
issues.push(normalized);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return issues;
|
|
592
|
+
}
|
|
593
|
+
async function fetchProjectItemsPage(config, cursor, fetchImpl) {
|
|
594
|
+
const result = await executeGraphQLQueryWithMetadata(config, PROJECT_ITEMS_QUERY, {
|
|
595
|
+
projectId: config.projectId,
|
|
596
|
+
cursor,
|
|
597
|
+
pageSize: config.pageSize ?? DEFAULT_PAGE_SIZE
|
|
598
|
+
}, fetchImpl);
|
|
599
|
+
const data = result.data;
|
|
600
|
+
const items = data.node?.items;
|
|
601
|
+
if (!items) {
|
|
602
|
+
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include project items.");
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
page: items,
|
|
606
|
+
rateLimits: result.rateLimits
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
var fetchGithubProjectIssues = fetchProjectIssues;
|
|
610
|
+
var fetchGithubIssueStatesByIds = fetchIssueStatesByIds;
|
|
611
|
+
async function fetchCurrentUserLogin(config, fetchImpl) {
|
|
612
|
+
const response = await fetchImpl(resolveRestUserApiUrl(config.apiUrl), {
|
|
613
|
+
method: "GET",
|
|
614
|
+
headers: {
|
|
615
|
+
authorization: `Bearer ${config.token}`,
|
|
616
|
+
"user-agent": "gh-symphony",
|
|
617
|
+
accept: "application/vnd.github+json"
|
|
618
|
+
},
|
|
619
|
+
signal: buildRequestSignal(config.timeoutMs)
|
|
620
|
+
});
|
|
621
|
+
if (!response.ok) {
|
|
622
|
+
const details = await response.text();
|
|
623
|
+
throw new GitHubTrackerHttpError(`GitHub REST request failed with status ${response.status}`, response.status, details);
|
|
624
|
+
}
|
|
625
|
+
const payload = await response.json();
|
|
626
|
+
if (!payload.login) {
|
|
627
|
+
throw new GitHubTrackerQueryError("GitHub REST response did not include the authenticated user login.");
|
|
628
|
+
}
|
|
629
|
+
return payload.login;
|
|
630
|
+
}
|
|
631
|
+
function isIssueAssignedToLogin(item, login) {
|
|
632
|
+
if (item.content?.__typename !== "Issue") {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
return (item.content.assignees?.nodes ?? []).some((assignee) => assignee?.login === login);
|
|
636
|
+
}
|
|
637
|
+
function emitAssignedOnlyFilterEvent(input) {
|
|
638
|
+
console.info(JSON.stringify({
|
|
639
|
+
event: "tracker-assigned-only-filtered",
|
|
640
|
+
projectId: input.projectId,
|
|
641
|
+
currentUserLogin: input.currentUserLogin,
|
|
642
|
+
includedCount: input.includedCount,
|
|
643
|
+
excludedCount: input.excludedCount
|
|
644
|
+
}));
|
|
645
|
+
}
|
|
646
|
+
function extractFieldValues(nodes) {
|
|
647
|
+
return nodes.reduce((values, node) => {
|
|
648
|
+
const fieldName = node?.field?.name;
|
|
649
|
+
if (!fieldName) {
|
|
650
|
+
return values;
|
|
651
|
+
}
|
|
652
|
+
if (node.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.name) {
|
|
653
|
+
values[fieldName] = node.name;
|
|
654
|
+
}
|
|
655
|
+
if (node.__typename === "ProjectV2ItemFieldTextValue" && node.text) {
|
|
656
|
+
values[fieldName] = node.text;
|
|
657
|
+
}
|
|
658
|
+
return values;
|
|
659
|
+
}, {});
|
|
660
|
+
}
|
|
661
|
+
function normalizeIssueStateLookupNode(projectId, issue, projectItem, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, rateLimits = null) {
|
|
662
|
+
if (issue?.__typename !== "Issue") {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
if (!projectItem) {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
const fieldValues = extractFieldValues(projectItem.fieldValues?.nodes ?? []);
|
|
669
|
+
const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
|
|
670
|
+
const repository = issue.repository;
|
|
671
|
+
const identifier = `${repository.owner.login}/${repository.name}#${issue.number}`;
|
|
672
|
+
return {
|
|
673
|
+
id: issue.id,
|
|
674
|
+
identifier,
|
|
675
|
+
number: issue.number,
|
|
676
|
+
title: identifier,
|
|
677
|
+
description: null,
|
|
678
|
+
priority: null,
|
|
679
|
+
state,
|
|
680
|
+
branchName: null,
|
|
681
|
+
url: `${repository.url}/issues/${issue.number}`,
|
|
682
|
+
labels: [],
|
|
683
|
+
blockedBy: [],
|
|
684
|
+
createdAt: null,
|
|
685
|
+
updatedAt: projectItem.updatedAt ?? issue.updatedAt,
|
|
686
|
+
repository: {
|
|
687
|
+
owner: repository.owner.login,
|
|
688
|
+
name: repository.name,
|
|
689
|
+
url: repository.url,
|
|
690
|
+
cloneUrl: deriveCloneUrl(repository.url)
|
|
691
|
+
},
|
|
692
|
+
tracker: {
|
|
693
|
+
adapter: "github-project",
|
|
694
|
+
bindingId: projectId,
|
|
695
|
+
itemId: projectItem.id
|
|
696
|
+
},
|
|
697
|
+
metadata: fieldValues,
|
|
698
|
+
rateLimits
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
async function resolveIssueProjectItemForStateLookup(config, issue, fetchImpl) {
|
|
702
|
+
if (issue?.__typename !== "Issue") {
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
let connection = issue.projectItems;
|
|
706
|
+
let projectItem = findProjectItemByProjectId(connection?.nodes ?? [], config.projectId);
|
|
707
|
+
let cursor = connection?.pageInfo.endCursor ?? null;
|
|
708
|
+
while (!projectItem && connection?.pageInfo.hasNextPage) {
|
|
709
|
+
const nextPage = await fetchIssueProjectItemsPage(config, issue.id, cursor, fetchImpl);
|
|
710
|
+
projectItem = findProjectItemByProjectId(nextPage.nodes ?? [], config.projectId);
|
|
711
|
+
connection = nextPage;
|
|
712
|
+
cursor = nextPage.pageInfo.endCursor;
|
|
713
|
+
}
|
|
714
|
+
return projectItem;
|
|
715
|
+
}
|
|
716
|
+
async function fetchIssueProjectItemsPage(config, issueId, cursor, fetchImpl) {
|
|
717
|
+
const result = await executeGraphQLQueryWithMetadata(config, ISSUE_PROJECT_ITEMS_PAGE_QUERY, {
|
|
718
|
+
issueId,
|
|
719
|
+
cursor
|
|
720
|
+
}, fetchImpl);
|
|
721
|
+
const data = result.data;
|
|
722
|
+
const issue = data.node;
|
|
723
|
+
if (issue?.__typename !== "Issue" || !issue.projectItems) {
|
|
724
|
+
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include issue project items.");
|
|
725
|
+
}
|
|
726
|
+
return issue.projectItems;
|
|
727
|
+
}
|
|
728
|
+
function findProjectItemByProjectId(nodes, projectId) {
|
|
729
|
+
return nodes.find((item) => item?.project?.id === projectId) ?? null;
|
|
730
|
+
}
|
|
731
|
+
function resolvePriority(item, priority) {
|
|
732
|
+
if (!priority.fieldName || !priority.optionIds) {
|
|
733
|
+
return null;
|
|
734
|
+
}
|
|
735
|
+
for (const node of item.fieldValues?.nodes ?? []) {
|
|
736
|
+
if (node?.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.field?.name === priority.fieldName && node.optionId) {
|
|
737
|
+
return priority.optionIds[node.optionId] ?? null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
function extractPriorityOptionOrder(fields, priorityFieldName) {
|
|
743
|
+
for (const field of fields) {
|
|
744
|
+
if (isSingleSelectProjectField(field) && field.name === priorityFieldName) {
|
|
745
|
+
let nextPriority = 0;
|
|
746
|
+
const optionEntries = (field.options ?? []).flatMap((option) => {
|
|
747
|
+
if (!option?.id) {
|
|
748
|
+
return [];
|
|
749
|
+
}
|
|
750
|
+
const entry = [option.id, nextPriority];
|
|
751
|
+
nextPriority += 1;
|
|
752
|
+
return [entry];
|
|
753
|
+
});
|
|
754
|
+
return Object.fromEntries(optionEntries);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return void 0;
|
|
758
|
+
}
|
|
759
|
+
async function fetchPriorityOptionOrder(config, priorityFieldName, fetchImpl) {
|
|
760
|
+
const data = await executeGraphQLQuery(config, PROJECT_FIELDS_QUERY, { projectId: config.projectId }, fetchImpl);
|
|
761
|
+
return extractPriorityOptionOrder(data.node?.fields?.nodes ?? [], priorityFieldName);
|
|
762
|
+
}
|
|
763
|
+
function isSingleSelectProjectField(field) {
|
|
764
|
+
return field?.__typename === "ProjectV2SingleSelectField";
|
|
765
|
+
}
|
|
766
|
+
function deriveCloneUrl(repositoryUrl) {
|
|
767
|
+
if (repositoryUrl.startsWith("file://") || repositoryUrl.endsWith(".git")) {
|
|
768
|
+
return repositoryUrl;
|
|
769
|
+
}
|
|
770
|
+
return `${repositoryUrl}.git`;
|
|
771
|
+
}
|
|
772
|
+
function normalizeBlockerState(state, lifecycle) {
|
|
773
|
+
if (!state) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
const normalized = state.trim().toLowerCase();
|
|
777
|
+
if (normalized === "closed") {
|
|
778
|
+
return lifecycle.terminalStates[0] ?? state;
|
|
779
|
+
}
|
|
780
|
+
if (normalized === "open") {
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
return state;
|
|
784
|
+
}
|
|
785
|
+
function resolveRestUserApiUrl(apiUrl) {
|
|
786
|
+
const parsed = new URL(apiUrl ?? DEFAULT_API_URL);
|
|
787
|
+
const pathSegments = parsed.pathname.split("/").filter(Boolean);
|
|
788
|
+
if (pathSegments.at(-1) === "graphql") {
|
|
789
|
+
pathSegments.pop();
|
|
790
|
+
}
|
|
791
|
+
parsed.pathname = `/${pathSegments.join("/")}/user`.replace(/\/{2,}/g, "/");
|
|
792
|
+
parsed.search = "";
|
|
793
|
+
parsed.hash = "";
|
|
794
|
+
return parsed.toString();
|
|
795
|
+
}
|
|
796
|
+
function chunkValues(values, size) {
|
|
797
|
+
const chunks = [];
|
|
798
|
+
for (let index = 0; index < values.length; index += size) {
|
|
799
|
+
chunks.push(values.slice(index, index + size));
|
|
800
|
+
}
|
|
801
|
+
return chunks;
|
|
802
|
+
}
|
|
803
|
+
function buildRequestSignal(timeoutMs) {
|
|
804
|
+
return AbortSignal.timeout(resolveNetworkTimeoutMs(timeoutMs));
|
|
805
|
+
}
|
|
806
|
+
function resolveNetworkTimeoutMs(timeoutMs) {
|
|
807
|
+
if (timeoutMs !== void 0 && Number.isInteger(timeoutMs) && timeoutMs > 0) {
|
|
808
|
+
return timeoutMs;
|
|
809
|
+
}
|
|
810
|
+
return DEFAULT_NETWORK_TIMEOUT_MS;
|
|
811
|
+
}
|
|
812
|
+
async function executeGraphQLQuery(config, query, variables, fetchImpl) {
|
|
813
|
+
const result = await executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl);
|
|
814
|
+
return result.data;
|
|
815
|
+
}
|
|
816
|
+
async function executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl) {
|
|
817
|
+
const response = await fetchImpl(config.apiUrl ?? DEFAULT_API_URL, {
|
|
818
|
+
method: "POST",
|
|
819
|
+
headers: {
|
|
820
|
+
"content-type": "application/json",
|
|
821
|
+
authorization: `Bearer ${config.token}`
|
|
822
|
+
},
|
|
823
|
+
body: JSON.stringify({
|
|
824
|
+
query,
|
|
825
|
+
variables
|
|
826
|
+
}),
|
|
827
|
+
signal: buildRequestSignal(config.timeoutMs)
|
|
828
|
+
});
|
|
829
|
+
if (!response.ok) {
|
|
830
|
+
const details = await response.text();
|
|
831
|
+
throw new GitHubTrackerHttpError(`GitHub GraphQL request failed with status ${response.status}`, response.status, details);
|
|
832
|
+
}
|
|
833
|
+
const payload = await response.json();
|
|
834
|
+
if (payload.errors?.length) {
|
|
835
|
+
throw new GitHubTrackerQueryError(payload.errors.map((error) => error.message).join("; "));
|
|
836
|
+
}
|
|
837
|
+
if (!payload.data) {
|
|
838
|
+
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include data.");
|
|
839
|
+
}
|
|
840
|
+
const data = payload.data;
|
|
841
|
+
return {
|
|
842
|
+
data,
|
|
843
|
+
rateLimits: extractGitHubRateLimits(response.headers)
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
function extractGitHubRateLimits(headers) {
|
|
847
|
+
if (!headers || typeof headers.get !== "function") {
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
const limit = parseIntegerHeader(headers.get("x-ratelimit-limit"));
|
|
851
|
+
const remaining = parseIntegerHeader(headers.get("x-ratelimit-remaining"));
|
|
852
|
+
const used = parseIntegerHeader(headers.get("x-ratelimit-used"));
|
|
853
|
+
const reset = parseIntegerHeader(headers.get("x-ratelimit-reset"));
|
|
854
|
+
const resource = headers.get("x-ratelimit-resource");
|
|
855
|
+
if (limit === null && remaining === null && used === null && reset === null && resource === null) {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
return {
|
|
859
|
+
source: "github",
|
|
860
|
+
limit,
|
|
861
|
+
remaining,
|
|
862
|
+
used,
|
|
863
|
+
reset,
|
|
864
|
+
resetAt: reset === null ? null : new Date(reset * 1e3).toISOString(),
|
|
865
|
+
resource
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
function parseIntegerHeader(value) {
|
|
869
|
+
if (value === null) {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
const parsed = Number.parseInt(value, 10);
|
|
873
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
874
|
+
}
|
|
875
|
+
var PROJECT_ITEMS_QUERY = `
|
|
876
|
+
query ProjectItems($projectId: ID!, $cursor: String, $pageSize: Int!) {
|
|
877
|
+
node(id: $projectId) {
|
|
878
|
+
__typename
|
|
879
|
+
... on ProjectV2 {
|
|
880
|
+
items(first: $pageSize, after: $cursor) {
|
|
881
|
+
nodes {
|
|
882
|
+
id
|
|
883
|
+
updatedAt
|
|
884
|
+
fieldValues(first: 20) {
|
|
885
|
+
nodes {
|
|
886
|
+
__typename
|
|
887
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
888
|
+
name
|
|
889
|
+
optionId
|
|
890
|
+
field {
|
|
891
|
+
... on ProjectV2SingleSelectField {
|
|
892
|
+
name
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
... on ProjectV2ItemFieldTextValue {
|
|
897
|
+
text
|
|
898
|
+
field {
|
|
899
|
+
... on ProjectV2FieldCommon {
|
|
900
|
+
name
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
content {
|
|
907
|
+
__typename
|
|
908
|
+
... on Issue {
|
|
909
|
+
id
|
|
910
|
+
number
|
|
911
|
+
title
|
|
912
|
+
body
|
|
913
|
+
url
|
|
914
|
+
createdAt
|
|
915
|
+
updatedAt
|
|
916
|
+
labels(first: 20) {
|
|
917
|
+
nodes {
|
|
918
|
+
name
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
assignees(first: 20) {
|
|
922
|
+
nodes {
|
|
923
|
+
login
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
repository {
|
|
927
|
+
name
|
|
928
|
+
url
|
|
929
|
+
owner {
|
|
930
|
+
login
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
blockedBy(first: 100) {
|
|
934
|
+
nodes {
|
|
935
|
+
id
|
|
936
|
+
number
|
|
937
|
+
state
|
|
938
|
+
repository {
|
|
939
|
+
name
|
|
940
|
+
owner {
|
|
941
|
+
login
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
pageInfo {
|
|
950
|
+
endCursor
|
|
951
|
+
hasNextPage
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
`;
|
|
958
|
+
var PROJECT_FIELDS_QUERY = `
|
|
959
|
+
query ProjectFields($projectId: ID!) {
|
|
960
|
+
node(id: $projectId) {
|
|
961
|
+
__typename
|
|
962
|
+
... on ProjectV2 {
|
|
963
|
+
fields(first: 100) {
|
|
964
|
+
nodes {
|
|
965
|
+
__typename
|
|
966
|
+
... on ProjectV2SingleSelectField {
|
|
967
|
+
name
|
|
968
|
+
options {
|
|
969
|
+
id
|
|
970
|
+
name
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
`;
|
|
979
|
+
var ISSUE_STATES_BY_IDS_QUERY = `
|
|
980
|
+
query IssueStatesByIds($issueIds: [ID!]!) {
|
|
981
|
+
nodes(ids: $issueIds) {
|
|
982
|
+
__typename
|
|
983
|
+
... on Issue {
|
|
984
|
+
id
|
|
985
|
+
number
|
|
986
|
+
updatedAt
|
|
987
|
+
repository {
|
|
988
|
+
name
|
|
989
|
+
url
|
|
990
|
+
owner {
|
|
991
|
+
login
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
projectItems(first: 100, includeArchived: false) {
|
|
995
|
+
nodes {
|
|
996
|
+
id
|
|
997
|
+
updatedAt
|
|
998
|
+
project {
|
|
999
|
+
id
|
|
1000
|
+
}
|
|
1001
|
+
fieldValues(first: 20) {
|
|
1002
|
+
nodes {
|
|
1003
|
+
__typename
|
|
1004
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
1005
|
+
name
|
|
1006
|
+
optionId
|
|
1007
|
+
field {
|
|
1008
|
+
... on ProjectV2SingleSelectField {
|
|
1009
|
+
name
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
... on ProjectV2ItemFieldTextValue {
|
|
1014
|
+
text
|
|
1015
|
+
field {
|
|
1016
|
+
... on ProjectV2FieldCommon {
|
|
1017
|
+
name
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
pageInfo {
|
|
1025
|
+
endCursor
|
|
1026
|
+
hasNextPage
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
`;
|
|
1033
|
+
var ISSUE_PROJECT_ITEMS_PAGE_QUERY = `
|
|
1034
|
+
query IssueProjectItemsPage($issueId: ID!, $cursor: String) {
|
|
1035
|
+
node(id: $issueId) {
|
|
1036
|
+
__typename
|
|
1037
|
+
... on Issue {
|
|
1038
|
+
id
|
|
1039
|
+
number
|
|
1040
|
+
updatedAt
|
|
1041
|
+
repository {
|
|
1042
|
+
name
|
|
1043
|
+
url
|
|
1044
|
+
owner {
|
|
1045
|
+
login
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
projectItems(first: 100, after: $cursor, includeArchived: false) {
|
|
1049
|
+
nodes {
|
|
1050
|
+
id
|
|
1051
|
+
updatedAt
|
|
1052
|
+
project {
|
|
1053
|
+
id
|
|
1054
|
+
}
|
|
1055
|
+
fieldValues(first: 20) {
|
|
1056
|
+
nodes {
|
|
1057
|
+
__typename
|
|
1058
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
1059
|
+
name
|
|
1060
|
+
optionId
|
|
1061
|
+
field {
|
|
1062
|
+
... on ProjectV2SingleSelectField {
|
|
1063
|
+
name
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
... on ProjectV2ItemFieldTextValue {
|
|
1068
|
+
text
|
|
1069
|
+
field {
|
|
1070
|
+
... on ProjectV2FieldCommon {
|
|
1071
|
+
name
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
pageInfo {
|
|
1079
|
+
endCursor
|
|
1080
|
+
hasNextPage
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
`;
|
|
1087
|
+
|
|
1088
|
+
// ../tracker-github/dist/orchestrator-adapter.js
|
|
1089
|
+
import { createHash } from "crypto";
|
|
1090
|
+
var githubProjectTrackerAdapter = {
|
|
1091
|
+
async listIssues(project, dependencies = {}) {
|
|
1092
|
+
return listProjectIssues(project, dependencies);
|
|
1093
|
+
},
|
|
1094
|
+
async listIssuesByStates(project, states, dependencies = {}) {
|
|
1095
|
+
if (states.length === 0) {
|
|
1096
|
+
return [];
|
|
1097
|
+
}
|
|
1098
|
+
const issues = await listProjectIssues(project, dependencies);
|
|
1099
|
+
const normalizedStates = new Set(states.map((state) => state.trim().toLowerCase()));
|
|
1100
|
+
return issues.filter((issue) => normalizedStates.has(issue.state.trim().toLowerCase()));
|
|
1101
|
+
},
|
|
1102
|
+
async fetchIssueStatesByIds(project, issueIds, dependencies = {}) {
|
|
1103
|
+
if (issueIds.length === 0) {
|
|
1104
|
+
return [];
|
|
1105
|
+
}
|
|
1106
|
+
return fetchProjectIssueStatesByIds(project, issueIds, dependencies);
|
|
1107
|
+
},
|
|
1108
|
+
buildWorkerEnvironment(project) {
|
|
1109
|
+
return {
|
|
1110
|
+
GITHUB_PROJECT_ID: requireTrackerSetting(project.tracker, "projectId")
|
|
1111
|
+
};
|
|
1112
|
+
},
|
|
1113
|
+
reviveIssue(project, run) {
|
|
1114
|
+
return {
|
|
1115
|
+
id: run.issueId,
|
|
1116
|
+
identifier: run.issueIdentifier,
|
|
1117
|
+
number: parseIssueNumber(run.issueIdentifier),
|
|
1118
|
+
title: run.issueTitle ?? run.issueIdentifier,
|
|
1119
|
+
description: null,
|
|
1120
|
+
priority: null,
|
|
1121
|
+
state: run.issueState,
|
|
1122
|
+
branchName: null,
|
|
1123
|
+
url: null,
|
|
1124
|
+
labels: [],
|
|
1125
|
+
blockedBy: [],
|
|
1126
|
+
createdAt: null,
|
|
1127
|
+
updatedAt: null,
|
|
1128
|
+
repository: run.repository,
|
|
1129
|
+
tracker: {
|
|
1130
|
+
adapter: "github-project",
|
|
1131
|
+
bindingId: project.tracker.bindingId,
|
|
1132
|
+
itemId: run.issueId
|
|
1133
|
+
},
|
|
1134
|
+
metadata: {}
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
};
|
|
1138
|
+
async function listProjectIssues(project, dependencies = {}) {
|
|
1139
|
+
const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
|
|
1140
|
+
const loadProjectIssues = () => fetchGithubProjectIssues(trackerConfig, dependencies.fetchImpl);
|
|
1141
|
+
return dependencies.projectItemsCache?.getOrLoad(buildProjectItemsCacheKey(trackerConfig, dependencies), loadProjectIssues) ?? loadProjectIssues();
|
|
1142
|
+
}
|
|
1143
|
+
async function fetchProjectIssueStatesByIds(project, issueIds, dependencies = {}) {
|
|
1144
|
+
const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
|
|
1145
|
+
return fetchGithubIssueStatesByIds(trackerConfig, [...issueIds], dependencies.fetchImpl);
|
|
1146
|
+
}
|
|
1147
|
+
function resolveGitHubTrackerConfig(project, dependencies = {}) {
|
|
1148
|
+
const token = dependencies.token ?? process.env.GITHUB_GRAPHQL_TOKEN;
|
|
1149
|
+
if (!token) {
|
|
1150
|
+
throw new Error("GITHUB_GRAPHQL_TOKEN environment variable is required. Run 'gh auth token' or set the variable.");
|
|
1151
|
+
}
|
|
1152
|
+
const githubProjectId = requireTrackerSetting(project.tracker, "projectId");
|
|
1153
|
+
return {
|
|
1154
|
+
projectId: githubProjectId,
|
|
1155
|
+
token,
|
|
1156
|
+
apiUrl: project.tracker.apiUrl,
|
|
1157
|
+
assignedOnly: readBooleanTrackerSetting(project.tracker, "assignedOnly"),
|
|
1158
|
+
priorityFieldName: readOptionalStringTrackerSetting(project.tracker, "priorityFieldName"),
|
|
1159
|
+
timeoutMs: readNumberTrackerSetting(project.tracker, "timeoutMs")
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
function buildProjectItemsCacheKey(config, _dependencies) {
|
|
1163
|
+
return JSON.stringify({
|
|
1164
|
+
adapter: "github-project",
|
|
1165
|
+
apiUrl: config.apiUrl,
|
|
1166
|
+
assignedOnly: config.assignedOnly ?? false,
|
|
1167
|
+
priorityFieldName: config.priorityFieldName ?? null,
|
|
1168
|
+
projectId: config.projectId,
|
|
1169
|
+
timeoutMs: config.timeoutMs,
|
|
1170
|
+
tokenFingerprint: hashToken(config.token)
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
function hashToken(token) {
|
|
1174
|
+
if (!token) {
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
return createHash("sha256").update(token).digest("hex");
|
|
1178
|
+
}
|
|
1179
|
+
var trackerAdapters = {
|
|
1180
|
+
"github-project": githubProjectTrackerAdapter
|
|
1181
|
+
};
|
|
1182
|
+
function resolveTrackerAdapter(tracker) {
|
|
1183
|
+
const adapter = trackerAdapters[tracker.adapter];
|
|
1184
|
+
if (!adapter) {
|
|
1185
|
+
throw new Error(`Unsupported tracker adapter: ${tracker.adapter}`);
|
|
1186
|
+
}
|
|
1187
|
+
return adapter;
|
|
1188
|
+
}
|
|
1189
|
+
function requireTrackerSetting(tracker, key) {
|
|
1190
|
+
const value = tracker.settings?.[key];
|
|
1191
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1192
|
+
throw new Error(`Tracker adapter "${tracker.adapter}" requires the "${key}" setting.`);
|
|
1193
|
+
}
|
|
1194
|
+
return value;
|
|
1195
|
+
}
|
|
1196
|
+
function readBooleanTrackerSetting(tracker, key) {
|
|
1197
|
+
const value = tracker.settings?.[key];
|
|
1198
|
+
return value === true || value === "true";
|
|
1199
|
+
}
|
|
1200
|
+
function readNumberTrackerSetting(tracker, key) {
|
|
1201
|
+
const value = tracker.settings?.[key];
|
|
1202
|
+
if (value === void 0) {
|
|
1203
|
+
return void 0;
|
|
1204
|
+
}
|
|
1205
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
1206
|
+
return value;
|
|
1207
|
+
}
|
|
1208
|
+
if (typeof value === "string") {
|
|
1209
|
+
const parsed = Number(value);
|
|
1210
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
1211
|
+
return parsed;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
throw new Error(`Tracker adapter "${tracker.adapter}" requires the "${key}" setting to be a positive integer when provided.`);
|
|
1215
|
+
}
|
|
1216
|
+
function readOptionalStringTrackerSetting(tracker, key) {
|
|
1217
|
+
const value = tracker.settings?.[key];
|
|
1218
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1219
|
+
}
|
|
1220
|
+
function parseIssueNumber(identifier) {
|
|
1221
|
+
const match = identifier.match(/#(\d+)$/);
|
|
1222
|
+
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// ../tracker-file/dist/file-tracker-adapter.js
|
|
1226
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1227
|
+
function requireTrackerSetting2(project, key) {
|
|
1228
|
+
const value = project.tracker.settings?.[key];
|
|
1229
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1230
|
+
throw new Error(`Tracker adapter "file" requires the "${key}" setting.`);
|
|
1231
|
+
}
|
|
1232
|
+
return value;
|
|
1233
|
+
}
|
|
1234
|
+
function parseIssueNumber2(identifier) {
|
|
1235
|
+
const match = identifier.match(/#(\d+)$/);
|
|
1236
|
+
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
1237
|
+
}
|
|
1238
|
+
function isValidIssueShape(entry) {
|
|
1239
|
+
if (!entry || typeof entry !== "object")
|
|
1240
|
+
return false;
|
|
1241
|
+
const e = entry;
|
|
1242
|
+
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";
|
|
1243
|
+
}
|
|
1244
|
+
var fileTrackerAdapter = {
|
|
1245
|
+
async listIssues(project) {
|
|
1246
|
+
const issuesPath = requireTrackerSetting2(project, "issuesPath");
|
|
1247
|
+
try {
|
|
1248
|
+
const raw = await readFile2(issuesPath, "utf-8");
|
|
1249
|
+
const parsed = JSON.parse(raw);
|
|
1250
|
+
if (!Array.isArray(parsed)) {
|
|
1251
|
+
throw new Error(`Expected an array of issues in ${issuesPath}, got ${typeof parsed}`);
|
|
1252
|
+
}
|
|
1253
|
+
const valid = [];
|
|
1254
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
1255
|
+
if (isValidIssueShape(parsed[i])) {
|
|
1256
|
+
valid.push(parsed[i]);
|
|
1257
|
+
} else {
|
|
1258
|
+
process.stderr.write(`[tracker-file] Skipping invalid issue at index ${i} in ${issuesPath}
|
|
1259
|
+
`);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return valid;
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
1265
|
+
return [];
|
|
1266
|
+
}
|
|
1267
|
+
if (err instanceof SyntaxError) {
|
|
1268
|
+
return [];
|
|
1269
|
+
}
|
|
1270
|
+
throw err;
|
|
1271
|
+
}
|
|
1272
|
+
},
|
|
1273
|
+
async listIssuesByStates(project, states) {
|
|
1274
|
+
if (states.length === 0) {
|
|
1275
|
+
return [];
|
|
1276
|
+
}
|
|
1277
|
+
const issues = await this.listIssues(project);
|
|
1278
|
+
const normalizedStates = new Set(states.map((state) => state.trim().toLowerCase()));
|
|
1279
|
+
return issues.filter((issue) => normalizedStates.has(issue.state.trim().toLowerCase()));
|
|
1280
|
+
},
|
|
1281
|
+
async fetchIssueStatesByIds(project, issueIds) {
|
|
1282
|
+
if (issueIds.length === 0) {
|
|
1283
|
+
return [];
|
|
1284
|
+
}
|
|
1285
|
+
const issues = await this.listIssues(project);
|
|
1286
|
+
const ids = new Set(issueIds);
|
|
1287
|
+
return issues.filter((issue) => ids.has(issue.id));
|
|
1288
|
+
},
|
|
1289
|
+
buildWorkerEnvironment(_project, _issue) {
|
|
1290
|
+
return {
|
|
1291
|
+
SYMPHONY_FILE_TRACKER: "true"
|
|
1292
|
+
};
|
|
1293
|
+
},
|
|
1294
|
+
reviveIssue(project, run) {
|
|
1295
|
+
return {
|
|
1296
|
+
id: run.issueId,
|
|
1297
|
+
identifier: run.issueIdentifier,
|
|
1298
|
+
number: parseIssueNumber2(run.issueIdentifier),
|
|
1299
|
+
title: run.issueTitle ?? run.issueIdentifier,
|
|
1300
|
+
description: null,
|
|
1301
|
+
priority: null,
|
|
1302
|
+
state: run.issueState,
|
|
1303
|
+
branchName: null,
|
|
1304
|
+
url: null,
|
|
1305
|
+
labels: [],
|
|
1306
|
+
blockedBy: [],
|
|
1307
|
+
createdAt: null,
|
|
1308
|
+
updatedAt: null,
|
|
1309
|
+
repository: run.repository,
|
|
1310
|
+
tracker: {
|
|
1311
|
+
adapter: "file",
|
|
1312
|
+
bindingId: project.tracker.bindingId,
|
|
1313
|
+
itemId: run.issueId
|
|
1314
|
+
},
|
|
1315
|
+
metadata: {}
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
|
|
1320
|
+
// ../orchestrator/dist/tracker-adapters.js
|
|
1321
|
+
var localAdapters = /* @__PURE__ */ new Map([
|
|
1322
|
+
["file", fileTrackerAdapter]
|
|
1323
|
+
]);
|
|
1324
|
+
function resolveTrackerAdapter2(tracker) {
|
|
1325
|
+
const local = localAdapters.get(tracker.adapter);
|
|
1326
|
+
if (local)
|
|
1327
|
+
return local;
|
|
1328
|
+
return resolveTrackerAdapter(tracker);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// ../orchestrator/dist/service.js
|
|
1332
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
1333
|
+
var DEFAULT_CONCURRENCY = 3;
|
|
1334
|
+
var DEFAULT_RETRY_BACKOFF_MS = 3e4;
|
|
1335
|
+
var CONTINUATION_RETRY_DELAY_MS = 1e3;
|
|
1336
|
+
var DEFAULT_WORKER_COMMAND = "node packages/worker/dist/index.js";
|
|
1337
|
+
var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
|
|
1338
|
+
var STUCK_WORKER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
1339
|
+
function isUsableWorkflowResolution(resolution) {
|
|
1340
|
+
return resolution.isValid || resolution.usedLastKnownGood;
|
|
1341
|
+
}
|
|
1342
|
+
function parseTimestampMs(value) {
|
|
1343
|
+
if (!value) {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
const parsed = new Date(value).getTime();
|
|
1347
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1348
|
+
}
|
|
1349
|
+
var OrchestratorService = class {
|
|
1350
|
+
store;
|
|
1351
|
+
projectConfig;
|
|
1352
|
+
dependencies;
|
|
1353
|
+
projectPollIntervals = /* @__PURE__ */ new Map();
|
|
1354
|
+
activeWorkerPids = /* @__PURE__ */ new Set();
|
|
1355
|
+
workerStderrBuffers = /* @__PURE__ */ new Map();
|
|
1356
|
+
workerStderrDecoders = /* @__PURE__ */ new Map();
|
|
1357
|
+
lastKnownGoodWorkflows = /* @__PURE__ */ new Map();
|
|
1358
|
+
lastReportedWorkflowErrors = /* @__PURE__ */ new Map();
|
|
1359
|
+
workflowResolutionCache = null;
|
|
1360
|
+
running = true;
|
|
1361
|
+
shuttingDown = false;
|
|
1362
|
+
shutdownPromise = null;
|
|
1363
|
+
sleepTimer = null;
|
|
1364
|
+
sleepResolver = null;
|
|
1365
|
+
reconcilePromise = Promise.resolve();
|
|
1366
|
+
reconcileRequested = false;
|
|
1367
|
+
constructor(store, projectConfig, dependencies = {}) {
|
|
1368
|
+
this.store = store;
|
|
1369
|
+
this.projectConfig = projectConfig;
|
|
1370
|
+
this.dependencies = dependencies;
|
|
1371
|
+
}
|
|
1372
|
+
async run(options = {}) {
|
|
1373
|
+
this.running = true;
|
|
1374
|
+
await this.runSerialized(() => this.performStartupCleanup(this.createTrackerDependencies()));
|
|
1375
|
+
while (this.running) {
|
|
1376
|
+
try {
|
|
1377
|
+
const snapshot = await this.runOnceInternal(options.issueIdentifier, this.createTrackerDependencies());
|
|
1378
|
+
await this.notifyTick(snapshot);
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
if (options.once) {
|
|
1381
|
+
throw error;
|
|
1382
|
+
}
|
|
1383
|
+
this.writeStderr(`[orchestrator] run loop failed for ${this.projectConfig.projectId}: ${this.formatErrorMessage(error)}`);
|
|
1384
|
+
}
|
|
1385
|
+
if (options.once || !this.running) {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
await this.waitForNextPoll();
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
async runOnce(options = {}) {
|
|
1392
|
+
return this.runOnceInternal(options.issueIdentifier, this.createTrackerDependencies());
|
|
1393
|
+
}
|
|
1394
|
+
async status() {
|
|
1395
|
+
return this.store.loadProjectStatus(this.projectConfig.projectId);
|
|
1396
|
+
}
|
|
1397
|
+
async statusForIssue(issueIdentifier) {
|
|
1398
|
+
const issueRecords = await this.store.loadProjectIssueOrchestrations(this.projectConfig.projectId);
|
|
1399
|
+
const issueRecord = issueRecords.find((record) => record.identifier === issueIdentifier);
|
|
1400
|
+
if (!issueRecord) {
|
|
1401
|
+
return null;
|
|
1402
|
+
}
|
|
1403
|
+
const currentRunCandidate = issueRecord.currentRunId ? await this.store.loadRun(issueRecord.currentRunId, this.projectConfig.projectId) : null;
|
|
1404
|
+
const currentRun = isMatchingIssueRun(currentRunCandidate, this.projectConfig.projectId, issueRecord.issueId, issueIdentifier) ? currentRunCandidate : await this.findLatestRunForIssue(issueRecord.issueId, issueIdentifier);
|
|
1405
|
+
const recentEvents = currentRun === null ? [] : await this.store.loadRecentRunEvents(currentRun.runId, 20, currentRun.projectId);
|
|
1406
|
+
const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
|
|
1407
|
+
const currentAttempt = currentRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
|
|
1408
|
+
return {
|
|
1409
|
+
issue_identifier: issueRecord.identifier,
|
|
1410
|
+
issue_id: issueRecord.issueId,
|
|
1411
|
+
status: currentRun?.status ?? mapIssueOrchestrationStateToStatus(issueRecord.state),
|
|
1412
|
+
workspace: {
|
|
1413
|
+
path: currentRun?.workingDirectory ?? null
|
|
1414
|
+
},
|
|
1415
|
+
attempts: {
|
|
1416
|
+
restart_count: Math.max(0, currentAttempt - 1),
|
|
1417
|
+
current_retry_attempt: currentAttempt
|
|
1418
|
+
},
|
|
1419
|
+
running: currentRun === null ? null : {
|
|
1420
|
+
session_id: currentRun.runtimeSession?.sessionId ?? null,
|
|
1421
|
+
turn_count: currentRun.turnCount ?? null,
|
|
1422
|
+
state: currentRun.issueState ?? null,
|
|
1423
|
+
started_at: currentRun.startedAt ?? null,
|
|
1424
|
+
last_event: currentRun.lastEvent ?? null,
|
|
1425
|
+
last_message: latestEventMessage,
|
|
1426
|
+
last_event_at: currentRun.lastEventAt ?? null,
|
|
1427
|
+
tokens: currentRun.tokenUsage ? {
|
|
1428
|
+
input_tokens: currentRun.tokenUsage.inputTokens,
|
|
1429
|
+
output_tokens: currentRun.tokenUsage.outputTokens,
|
|
1430
|
+
total_tokens: currentRun.tokenUsage.totalTokens
|
|
1431
|
+
} : null
|
|
1432
|
+
},
|
|
1433
|
+
retry: currentRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ? {
|
|
1434
|
+
due_at: currentRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ?? "",
|
|
1435
|
+
kind: currentRun?.retryKind ?? null,
|
|
1436
|
+
error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null
|
|
1437
|
+
} : null,
|
|
1438
|
+
logs: {
|
|
1439
|
+
codex_session_logs: currentRun === null ? [] : [
|
|
1440
|
+
{
|
|
1441
|
+
label: "worker",
|
|
1442
|
+
path: join3(this.store.runDir(currentRun.runId, currentRun.projectId), "worker.log"),
|
|
1443
|
+
url: null
|
|
1444
|
+
}
|
|
1445
|
+
]
|
|
1446
|
+
},
|
|
1447
|
+
recent_events: recentEvents,
|
|
1448
|
+
last_error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null,
|
|
1449
|
+
tracked: {
|
|
1450
|
+
issue_orchestration_state: issueRecord.state,
|
|
1451
|
+
current_run_id: issueRecord.currentRunId,
|
|
1452
|
+
workspace_key: issueRecord.workspaceKey,
|
|
1453
|
+
run_phase: currentRun?.runPhase ?? null,
|
|
1454
|
+
execution_phase: currentRun?.executionPhase ?? null
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
async recover() {
|
|
1459
|
+
return this.runOnce();
|
|
1460
|
+
}
|
|
1461
|
+
requestReconcile() {
|
|
1462
|
+
this.reconcileRequested = true;
|
|
1463
|
+
this.cancelPendingSleep();
|
|
1464
|
+
}
|
|
1465
|
+
async shutdown() {
|
|
1466
|
+
if (this.shutdownPromise) {
|
|
1467
|
+
return this.shutdownPromise;
|
|
1468
|
+
}
|
|
1469
|
+
this.shuttingDown = true;
|
|
1470
|
+
this.shutdownPromise = (async () => {
|
|
1471
|
+
this.running = false;
|
|
1472
|
+
this.cancelPendingSleep();
|
|
1473
|
+
const workerPids = [...this.activeWorkerPids];
|
|
1474
|
+
for (const pid of workerPids) {
|
|
1475
|
+
this.sendSignal(pid, "SIGTERM");
|
|
1476
|
+
}
|
|
1477
|
+
if (workerPids.length === 0) {
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
let waitedMs = 0;
|
|
1481
|
+
while (this.activeWorkerPids.size > 0 && waitedMs < 1e4) {
|
|
1482
|
+
this.pruneExitedWorkerPids();
|
|
1483
|
+
if (this.activeWorkerPids.size === 0) {
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
await (this.dependencies.waitImpl ?? wait2)(100);
|
|
1487
|
+
waitedMs += 100;
|
|
1488
|
+
}
|
|
1489
|
+
for (const pid of [...this.activeWorkerPids]) {
|
|
1490
|
+
if (!this.isProcessRunning(pid)) {
|
|
1491
|
+
this.retireWorkerPid(pid);
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
this.sendSignal(pid, "SIGKILL");
|
|
1495
|
+
this.retireWorkerPid(pid);
|
|
1496
|
+
}
|
|
1497
|
+
})();
|
|
1498
|
+
return this.shutdownPromise;
|
|
1499
|
+
}
|
|
1500
|
+
getEffectivePollIntervalMs() {
|
|
1501
|
+
if (this.dependencies.pollIntervalMs) {
|
|
1502
|
+
return this.dependencies.pollIntervalMs;
|
|
1503
|
+
}
|
|
1504
|
+
const configuredIntervals = [...this.projectPollIntervals.values()].filter((value) => Number.isFinite(value) && value > 0);
|
|
1505
|
+
return configuredIntervals.length ? Math.min(...configuredIntervals) : DEFAULT_POLL_INTERVAL_MS;
|
|
1506
|
+
}
|
|
1507
|
+
async reconcileProject(tenant, issueIdentifier, trackerDependencies = {}) {
|
|
1508
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
1509
|
+
const now = this.now();
|
|
1510
|
+
let lastError = null;
|
|
1511
|
+
let dispatched = 0;
|
|
1512
|
+
let suppressed = 0;
|
|
1513
|
+
let recovered = 0;
|
|
1514
|
+
let pollIntervalMs = DEFAULT_POLL_INTERVAL_MS;
|
|
1515
|
+
let rateLimits = null;
|
|
1516
|
+
let issueRecords = await this.store.loadProjectIssueOrchestrations(tenant.projectId);
|
|
1517
|
+
const allRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
1518
|
+
const activeRuns = allRuns.filter((run) => isActiveRunStatus(run.status));
|
|
1519
|
+
for (const run of activeRuns) {
|
|
1520
|
+
const outcome = await this.reconcileRun(tenant, run, issueRecords);
|
|
1521
|
+
issueRecords = outcome.issueRecords;
|
|
1522
|
+
if (outcome.recovered) {
|
|
1523
|
+
recovered += 1;
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
const reconciledRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId && isActiveRunStatus(run.status));
|
|
1527
|
+
const projectRunsAfterReconcile = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
1528
|
+
rateLimits = resolveProjectRateLimits(reconciledRuns, []);
|
|
1529
|
+
try {
|
|
1530
|
+
pollIntervalMs = await this.loadProjectPollInterval(tenant);
|
|
1531
|
+
const currentActiveRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId && isActiveRunStatus(run.status));
|
|
1532
|
+
const { runs: syncedActiveRuns, issuesByIdentifier: syncedIssuesByIdentifier } = await this.syncActiveRunIssueStates(tenant, trackerAdapter, currentActiveRuns, now);
|
|
1533
|
+
const issues = await trackerAdapter.listIssues(tenant, trackerDependencies);
|
|
1534
|
+
const filteredIssues = issueIdentifier ? issues.filter((issue) => issue.identifier === issueIdentifier) : issues;
|
|
1535
|
+
const { candidates: actionableCandidates, lifecycle } = await this.resolveActionableCandidates(tenant, filteredIssues);
|
|
1536
|
+
const trackedIssuesByIdentifier = new Map(syncedIssuesByIdentifier);
|
|
1537
|
+
for (const issue of filteredIssues) {
|
|
1538
|
+
const existing = trackedIssuesByIdentifier.get(issue.identifier);
|
|
1539
|
+
trackedIssuesByIdentifier.set(issue.identifier, {
|
|
1540
|
+
...existing ?? issue,
|
|
1541
|
+
...issue,
|
|
1542
|
+
rateLimits: issue.rateLimits ?? existing?.rateLimits ?? null
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
for (const [identifier, issue] of syncedIssuesByIdentifier) {
|
|
1546
|
+
const existing = trackedIssuesByIdentifier.get(identifier);
|
|
1547
|
+
if (!existing) {
|
|
1548
|
+
trackedIssuesByIdentifier.set(identifier, issue);
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
trackedIssuesByIdentifier.set(identifier, {
|
|
1552
|
+
...issue,
|
|
1553
|
+
...existing,
|
|
1554
|
+
rateLimits: existing.rateLimits ?? issue.rateLimits ?? null
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
rateLimits = resolveProjectRateLimits(syncedActiveRuns, trackedIssuesByIdentifier.values());
|
|
1558
|
+
const concurrency = await this.getProjectConcurrency(tenant);
|
|
1559
|
+
const currentlyActive = issueRecords.filter((record) => isIssueOrchestrationClaimed(record.state)).length;
|
|
1560
|
+
const availableSlots = Math.max(0, concurrency - currentlyActive);
|
|
1561
|
+
const unscheduledCandidates = actionableCandidates.filter((issue) => {
|
|
1562
|
+
if (hasConvergenceLockedRun(projectRunsAfterReconcile, issue.id, issue.state)) {
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
return !issueRecords.some((record) => record.issueId === issue.id && isIssueOrchestrationClaimed(record.state));
|
|
1566
|
+
});
|
|
1567
|
+
const sortedCandidates = sortCandidatesForDispatch(unscheduledCandidates);
|
|
1568
|
+
const activeByState = /* @__PURE__ */ new Map();
|
|
1569
|
+
for (const run of syncedActiveRuns) {
|
|
1570
|
+
const state = run.issueState;
|
|
1571
|
+
const count = activeByState.get(state) ?? 0;
|
|
1572
|
+
activeByState.set(state, count + 1);
|
|
1573
|
+
}
|
|
1574
|
+
const maxConcurrentByState = await this.loadProjectMaxConcurrentByState(tenant);
|
|
1575
|
+
let slotsRemaining = availableSlots;
|
|
1576
|
+
for (const issue of sortedCandidates) {
|
|
1577
|
+
if (this.shuttingDown) {
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1580
|
+
if (slotsRemaining <= 0)
|
|
1581
|
+
break;
|
|
1582
|
+
if (isIssueBudgetExceeded(resolveIssueBudgetSnapshot(projectRunsAfterReconcile, issue.id), now)) {
|
|
1583
|
+
continue;
|
|
1584
|
+
}
|
|
1585
|
+
const stateLimit = maxConcurrentByState[issue.state];
|
|
1586
|
+
if (stateLimit !== void 0) {
|
|
1587
|
+
const activeInState = activeByState.get(issue.state) ?? 0;
|
|
1588
|
+
if (activeInState >= stateLimit) {
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey({
|
|
1593
|
+
projectId: tenant.projectId,
|
|
1594
|
+
adapter: issue.tracker.adapter,
|
|
1595
|
+
issueSubjectId: issue.id
|
|
1596
|
+
}, issue.identifier);
|
|
1597
|
+
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
1598
|
+
issueId: issue.id,
|
|
1599
|
+
identifier: issue.identifier,
|
|
1600
|
+
workspaceKey: preferredWorkspaceKey,
|
|
1601
|
+
state: "claimed",
|
|
1602
|
+
currentRunId: null,
|
|
1603
|
+
retryEntry: null,
|
|
1604
|
+
updatedAt: now.toISOString()
|
|
1605
|
+
});
|
|
1606
|
+
let run;
|
|
1607
|
+
try {
|
|
1608
|
+
run = await this.startRun(tenant, issue);
|
|
1609
|
+
} catch (error) {
|
|
1610
|
+
issueRecords = releaseIssueOrchestration(issueRecords, issue.id, now);
|
|
1611
|
+
throw error;
|
|
1612
|
+
}
|
|
1613
|
+
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
1614
|
+
issueId: run.issueId,
|
|
1615
|
+
identifier: run.issueIdentifier,
|
|
1616
|
+
workspaceKey: run.issueWorkspaceKey ?? preferredWorkspaceKey,
|
|
1617
|
+
state: "running",
|
|
1618
|
+
currentRunId: run.runId,
|
|
1619
|
+
retryEntry: null,
|
|
1620
|
+
updatedAt: now.toISOString()
|
|
1621
|
+
});
|
|
1622
|
+
await this.store.saveRun(run);
|
|
1623
|
+
await this.store.appendRunEvent(run.runId, {
|
|
1624
|
+
at: now.toISOString(),
|
|
1625
|
+
event: "run-dispatched",
|
|
1626
|
+
projectId: tenant.projectId,
|
|
1627
|
+
issueIdentifier: issue.identifier,
|
|
1628
|
+
issueId: run.issueId,
|
|
1629
|
+
issueState: issue.state
|
|
1630
|
+
});
|
|
1631
|
+
this.logVerbose(`[dispatch] Issue ${issue.identifier} \u2192 run ${run.runId}`);
|
|
1632
|
+
dispatched += 1;
|
|
1633
|
+
slotsRemaining -= 1;
|
|
1634
|
+
activeByState.set(issue.state, (activeByState.get(issue.state) ?? 0) + 1);
|
|
1635
|
+
}
|
|
1636
|
+
for (const issueRecord of issueRecords) {
|
|
1637
|
+
if (!isIssueOrchestrationClaimed(issueRecord.state)) {
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1640
|
+
const issue = trackedIssuesByIdentifier.get(issueRecord.identifier);
|
|
1641
|
+
if (!issue) {
|
|
1642
|
+
continue;
|
|
1643
|
+
}
|
|
1644
|
+
const persistedRun = issueRecord.currentRunId ? await this.store.loadRun(issueRecord.currentRunId, tenant.projectId) : null;
|
|
1645
|
+
const activeRun = syncedActiveRuns.find((run) => isMatchingIssueRun(run, tenant.projectId, issueRecord.issueId, issueRecord.identifier)) ?? persistedRun;
|
|
1646
|
+
const resolvedIssue = actionableCandidates.find((candidate) => candidate.identifier === issue.identifier);
|
|
1647
|
+
if (resolvedIssue) {
|
|
1648
|
+
continue;
|
|
1649
|
+
}
|
|
1650
|
+
if (activeRun?.processId) {
|
|
1651
|
+
this.sendSignal(activeRun.processId, "SIGTERM");
|
|
1652
|
+
this.retireWorkerPid(activeRun.processId);
|
|
1653
|
+
}
|
|
1654
|
+
if (activeRun) {
|
|
1655
|
+
const suppressedRun = {
|
|
1656
|
+
...activeRun,
|
|
1657
|
+
status: "suppressed",
|
|
1658
|
+
processId: null,
|
|
1659
|
+
completedAt: now.toISOString(),
|
|
1660
|
+
updatedAt: now.toISOString(),
|
|
1661
|
+
runPhase: "canceled_by_reconciliation",
|
|
1662
|
+
lastError: "Run suppressed because the tracker state is no longer actionable."
|
|
1663
|
+
};
|
|
1664
|
+
await this.store.saveRun(suppressedRun);
|
|
1665
|
+
this.logVerbose(`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`);
|
|
1666
|
+
}
|
|
1667
|
+
issueRecords = releaseIssueOrchestration(issueRecords, issueRecord.issueId, now);
|
|
1668
|
+
suppressed += 1;
|
|
1669
|
+
}
|
|
1670
|
+
const terminalIssuesByIdentifier = /* @__PURE__ */ new Map();
|
|
1671
|
+
for (const issue of trackedIssuesByIdentifier.values()) {
|
|
1672
|
+
if (!isStateTerminal(issue.state, lifecycle)) {
|
|
1673
|
+
continue;
|
|
1674
|
+
}
|
|
1675
|
+
terminalIssuesByIdentifier.set(issue.identifier, issue);
|
|
1676
|
+
}
|
|
1677
|
+
for (const issue of terminalIssuesByIdentifier.values()) {
|
|
1678
|
+
await this.cleanupTerminalIssueWorkspace(tenant, issue, now);
|
|
1679
|
+
}
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
lastError = error instanceof Error ? error.message : "Unknown orchestration error";
|
|
1682
|
+
}
|
|
1683
|
+
this.projectPollIntervals.set(tenant.projectId, pollIntervalMs);
|
|
1684
|
+
await this.store.saveProjectIssueOrchestrations(tenant.projectId, issueRecords);
|
|
1685
|
+
const allTenantRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
1686
|
+
const latestRuns = allTenantRuns.filter((run) => isActiveRunStatus(run.status));
|
|
1687
|
+
rateLimits = rateLimits ?? resolveProjectRateLimits(latestRuns, []);
|
|
1688
|
+
const status = buildProjectSnapshot({
|
|
1689
|
+
project: tenant,
|
|
1690
|
+
activeRuns: latestRuns,
|
|
1691
|
+
allRuns: allTenantRuns,
|
|
1692
|
+
summary: { dispatched, suppressed, recovered },
|
|
1693
|
+
lastTickAt: now.toISOString(),
|
|
1694
|
+
lastError,
|
|
1695
|
+
rateLimits
|
|
1696
|
+
});
|
|
1697
|
+
await this.store.saveProjectStatus(status);
|
|
1698
|
+
return status;
|
|
1699
|
+
}
|
|
1700
|
+
async performStartupCleanup(trackerDependencies = {}) {
|
|
1701
|
+
const tenant = this.projectConfig;
|
|
1702
|
+
const now = this.now();
|
|
1703
|
+
const workspaceRecords = await this.store.loadIssueWorkspaces(tenant.projectId);
|
|
1704
|
+
if (workspaceRecords.length === 0) {
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
1708
|
+
const workflowCache = /* @__PURE__ */ new Map();
|
|
1709
|
+
let issues;
|
|
1710
|
+
try {
|
|
1711
|
+
issues = await trackerAdapter.listIssuesByStates(tenant, await this.resolveStartupCleanupTerminalStates(tenant, workspaceRecords, workflowCache), trackerDependencies);
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
const message = error instanceof Error ? error.message : "Unknown tracker error";
|
|
1714
|
+
console.warn(`[orchestrator] Startup cleanup skipped for project ${tenant.projectId}: ${message}`);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
const issuesById = new Map(issues.map((issue) => [issue.id, issue]));
|
|
1718
|
+
for (const workspaceRecord of workspaceRecords) {
|
|
1719
|
+
if (workspaceRecord.status === "removed") {
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
const issue = issuesById.get(workspaceRecord.issueSubjectId);
|
|
1723
|
+
if (!issue) {
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
try {
|
|
1727
|
+
const resolution = await this.loadStartupCleanupWorkflow(tenant, issue.repository, workflowCache);
|
|
1728
|
+
if (!resolution.isValid) {
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
if (!isStateTerminal(issue.state, resolution.lifecycle)) {
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1734
|
+
await this.cleanupTerminalIssueWorkspace(tenant, issue, now, resolution);
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
const message = error instanceof Error ? error.message : "Unknown startup cleanup error";
|
|
1737
|
+
console.warn(`[orchestrator] Startup cleanup skipped workspace for ${issue.identifier}: ${message}`);
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
async notifyTick(snapshot) {
|
|
1742
|
+
if (!this.dependencies.onTick) {
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
try {
|
|
1746
|
+
await this.dependencies.onTick(snapshot);
|
|
1747
|
+
} catch (error) {
|
|
1748
|
+
this.writeStderr(`[orchestrator] onTick callback failed: ${this.formatErrorMessage(error)}`);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
formatErrorMessage(error) {
|
|
1752
|
+
if (error instanceof Error) {
|
|
1753
|
+
return error.stack ?? error.message;
|
|
1754
|
+
}
|
|
1755
|
+
return String(error);
|
|
1756
|
+
}
|
|
1757
|
+
async resolveStartupCleanupTerminalStates(tenant, workspaceRecords, workflowCache) {
|
|
1758
|
+
const terminalStates = /* @__PURE__ */ new Map();
|
|
1759
|
+
const repositories = this.resolveStartupCleanupRepositories(tenant, workspaceRecords);
|
|
1760
|
+
for (const repository of repositories) {
|
|
1761
|
+
let resolution;
|
|
1762
|
+
try {
|
|
1763
|
+
resolution = await this.loadStartupCleanupWorkflow(tenant, repository, workflowCache);
|
|
1764
|
+
} catch {
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
for (const state of resolution.lifecycle.terminalStates) {
|
|
1771
|
+
const normalizedState = state.trim().toLowerCase();
|
|
1772
|
+
if (!terminalStates.has(normalizedState)) {
|
|
1773
|
+
terminalStates.set(normalizedState, state);
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
if (terminalStates.size === 0) {
|
|
1778
|
+
for (const state of DEFAULT_WORKFLOW_LIFECYCLE.terminalStates) {
|
|
1779
|
+
terminalStates.set(state.trim().toLowerCase(), state);
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
return [...terminalStates.values()];
|
|
1783
|
+
}
|
|
1784
|
+
resolveStartupCleanupRepositories(tenant, workspaceRecords) {
|
|
1785
|
+
const repositories = /* @__PURE__ */ new Map();
|
|
1786
|
+
for (const repository of tenant.repositories) {
|
|
1787
|
+
repositories.set(this.startupCleanupRepositoryKey(repository.owner, repository.name), repository);
|
|
1788
|
+
}
|
|
1789
|
+
for (const workspaceRecord of workspaceRecords) {
|
|
1790
|
+
const repository = this.parseWorkspaceRepositoryRef(workspaceRecord);
|
|
1791
|
+
if (!repository) {
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
const key = this.startupCleanupRepositoryKey(repository.owner, repository.name);
|
|
1795
|
+
if (!repositories.has(key)) {
|
|
1796
|
+
repositories.set(key, repository);
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
return [...repositories.values()];
|
|
1800
|
+
}
|
|
1801
|
+
parseWorkspaceRepositoryRef(workspaceRecord) {
|
|
1802
|
+
const match = workspaceRecord.issueIdentifier.match(/^([^/]+)\/([^#]+)#\d+$/);
|
|
1803
|
+
if (!match) {
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
const owner = match[1];
|
|
1807
|
+
const name = match[2];
|
|
1808
|
+
if (!owner || !name) {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
return {
|
|
1812
|
+
owner,
|
|
1813
|
+
name,
|
|
1814
|
+
cloneUrl: workspaceRecord.repositoryPath
|
|
1815
|
+
};
|
|
1816
|
+
}
|
|
1817
|
+
startupCleanupRepositoryKey(owner, name) {
|
|
1818
|
+
return `${owner}/${name}`;
|
|
1819
|
+
}
|
|
1820
|
+
async loadStartupCleanupWorkflow(tenant, repository, workflowCache) {
|
|
1821
|
+
const cacheKey = this.workflowCacheKey(repository);
|
|
1822
|
+
const cachedResolution = workflowCache.get(cacheKey);
|
|
1823
|
+
if (cachedResolution) {
|
|
1824
|
+
return cachedResolution;
|
|
1825
|
+
}
|
|
1826
|
+
const resolutionPromise = tenant.repositories.some((candidate) => candidate.owner === repository.owner && candidate.name === repository.name) ? this.loadProjectWorkflow(tenant, repository) : loadRepositoryWorkflow(repository.cloneUrl, repository);
|
|
1827
|
+
workflowCache.set(cacheKey, resolutionPromise);
|
|
1828
|
+
return resolutionPromise;
|
|
1829
|
+
}
|
|
1830
|
+
async runSerialized(operation) {
|
|
1831
|
+
const previous = this.reconcilePromise;
|
|
1832
|
+
let release;
|
|
1833
|
+
this.reconcilePromise = new Promise((resolve4) => {
|
|
1834
|
+
release = resolve4;
|
|
1835
|
+
});
|
|
1836
|
+
await previous;
|
|
1837
|
+
try {
|
|
1838
|
+
return await operation();
|
|
1839
|
+
} finally {
|
|
1840
|
+
release();
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
async runOnceInternal(issueIdentifier, trackerDependencies) {
|
|
1844
|
+
return this.runSerialized(async () => {
|
|
1845
|
+
const workflowResolutionCache = /* @__PURE__ */ new Map();
|
|
1846
|
+
this.workflowResolutionCache = workflowResolutionCache;
|
|
1847
|
+
try {
|
|
1848
|
+
return await this.reconcileProject(this.projectConfig, issueIdentifier, trackerDependencies);
|
|
1849
|
+
} finally {
|
|
1850
|
+
if (this.workflowResolutionCache === workflowResolutionCache) {
|
|
1851
|
+
this.workflowResolutionCache = null;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
createTrackerDependencies() {
|
|
1857
|
+
return {
|
|
1858
|
+
fetchImpl: this.dependencies.fetchImpl,
|
|
1859
|
+
projectItemsCache: createProjectItemsCache()
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
async findLatestRunForIssue(issueId, issueIdentifier) {
|
|
1863
|
+
const matchingRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === this.projectConfig.projectId).filter((run) => run.issueId === issueId || run.issueIdentifier === issueIdentifier).sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
|
|
1864
|
+
return matchingRuns[0] ?? null;
|
|
1865
|
+
}
|
|
1866
|
+
async resolveActionableCandidates(tenant, issues) {
|
|
1867
|
+
const candidates = [];
|
|
1868
|
+
let lifecycle = null;
|
|
1869
|
+
for (const issue of issues) {
|
|
1870
|
+
const resolution = await this.loadProjectWorkflow(tenant, issue.repository);
|
|
1871
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
1872
|
+
continue;
|
|
1873
|
+
}
|
|
1874
|
+
if (!lifecycle) {
|
|
1875
|
+
lifecycle = resolution.lifecycle;
|
|
1876
|
+
}
|
|
1877
|
+
if (!isStateActive(issue.state, resolution.lifecycle)) {
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (matchesWorkflowState(issue.state, resolution.lifecycle.blockerCheckStates) && issue.blockedBy.length > 0) {
|
|
1881
|
+
const hasNonTerminalBlocker = issue.blockedBy.some((blockerRef) => {
|
|
1882
|
+
if (blockerRef.state && isStateTerminal(blockerRef.state, resolution.lifecycle)) {
|
|
1883
|
+
return false;
|
|
1884
|
+
}
|
|
1885
|
+
if (blockerRef.identifier) {
|
|
1886
|
+
const blockerIssue = issues.find((candidate) => candidate.identifier === blockerRef.identifier);
|
|
1887
|
+
if (blockerIssue?.state) {
|
|
1888
|
+
return !isStateTerminal(blockerIssue.state, resolution.lifecycle);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
return true;
|
|
1892
|
+
});
|
|
1893
|
+
if (hasNonTerminalBlocker) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
candidates.push(issue);
|
|
1898
|
+
}
|
|
1899
|
+
if (!lifecycle && tenant.repositories.length > 0) {
|
|
1900
|
+
const resolution = await this.loadProjectWorkflow(tenant, tenant.repositories[0]);
|
|
1901
|
+
if (isUsableWorkflowResolution(resolution)) {
|
|
1902
|
+
lifecycle = resolution.lifecycle;
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
return {
|
|
1906
|
+
candidates,
|
|
1907
|
+
lifecycle: lifecycle ?? {
|
|
1908
|
+
stateFieldName: "Status",
|
|
1909
|
+
activeStates: ["Todo", "In Progress"],
|
|
1910
|
+
terminalStates: ["Done"],
|
|
1911
|
+
blockerCheckStates: ["Todo"]
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
async loadProjectWorkflow(tenant, repository) {
|
|
1916
|
+
const cacheKey = this.workflowCacheKey(repository);
|
|
1917
|
+
const pendingCache = this.workflowResolutionCache;
|
|
1918
|
+
if (pendingCache) {
|
|
1919
|
+
const cachedResolution = pendingCache.get(cacheKey);
|
|
1920
|
+
if (cachedResolution) {
|
|
1921
|
+
return cachedResolution;
|
|
1922
|
+
}
|
|
1923
|
+
const resolutionPromise = this.loadProjectWorkflowUncached(tenant, repository);
|
|
1924
|
+
pendingCache.set(cacheKey, resolutionPromise);
|
|
1925
|
+
return resolutionPromise;
|
|
1926
|
+
}
|
|
1927
|
+
return this.loadProjectWorkflowUncached(tenant, repository);
|
|
1928
|
+
}
|
|
1929
|
+
async loadProjectWorkflowUncached(tenant, repository) {
|
|
1930
|
+
const cacheRoot = join3(this.store.projectDir(tenant.projectId), "cache", repository.owner, repository.name);
|
|
1931
|
+
const { repositoryDirectory, changed } = await syncRepositoryForRun({
|
|
1932
|
+
repository,
|
|
1933
|
+
targetDirectory: cacheRoot
|
|
1934
|
+
});
|
|
1935
|
+
const resolution = await loadRepositoryWorkflow(repositoryDirectory, repository);
|
|
1936
|
+
return this.resolveWorkflowResolution(repository, cacheRoot, resolution, changed);
|
|
1937
|
+
}
|
|
1938
|
+
async startRun(tenant, issue, resumeContext) {
|
|
1939
|
+
if (this.shuttingDown || !this.running) {
|
|
1940
|
+
throw new Error("Orchestrator is shutting down and cannot start new runs.");
|
|
1941
|
+
}
|
|
1942
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
1943
|
+
const now = this.now();
|
|
1944
|
+
const runId = createRunId(now, tenant.projectId, issue.identifier);
|
|
1945
|
+
const runDir = this.store.runDir(runId, tenant.projectId);
|
|
1946
|
+
const workspaceRuntimeDir = runDir;
|
|
1947
|
+
const issueSubjectId = issue.id;
|
|
1948
|
+
const identity = {
|
|
1949
|
+
projectId: tenant.projectId,
|
|
1950
|
+
adapter: issue.tracker.adapter,
|
|
1951
|
+
issueSubjectId
|
|
1952
|
+
};
|
|
1953
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(identity, issue.identifier);
|
|
1954
|
+
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(identity);
|
|
1955
|
+
const existingWorkspaceRecord = await this.store.loadIssueWorkspace(tenant.projectId, preferredWorkspaceKey) ?? (legacyWorkspaceKey === preferredWorkspaceKey ? null : await this.store.loadIssueWorkspace(tenant.projectId, legacyWorkspaceKey));
|
|
1956
|
+
const workspaceKey = existingWorkspaceRecord?.workspaceKey ?? preferredWorkspaceKey;
|
|
1957
|
+
const projectDir = this.store.projectDir(tenant.projectId);
|
|
1958
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(projectDir, workspaceKey);
|
|
1959
|
+
const repositoryDirectory = await ensureIssueWorkspaceRepository({
|
|
1960
|
+
repository: issue.repository,
|
|
1961
|
+
issueWorkspacePath
|
|
1962
|
+
});
|
|
1963
|
+
if (!existingWorkspaceRecord) {
|
|
1964
|
+
const workspaceRecord = {
|
|
1965
|
+
workspaceKey,
|
|
1966
|
+
projectId: tenant.projectId,
|
|
1967
|
+
adapter: issue.tracker.adapter,
|
|
1968
|
+
issueSubjectId,
|
|
1969
|
+
issueIdentifier: issue.identifier,
|
|
1970
|
+
workspacePath: issueWorkspacePath,
|
|
1971
|
+
repositoryPath: repositoryDirectory,
|
|
1972
|
+
status: "active",
|
|
1973
|
+
createdAt: now.toISOString(),
|
|
1974
|
+
updatedAt: now.toISOString(),
|
|
1975
|
+
lastError: null
|
|
1976
|
+
};
|
|
1977
|
+
await this.store.saveIssueWorkspace(workspaceRecord);
|
|
1978
|
+
const afterCreateResult = await this.runHook("after_create", tenant, repositoryDirectory, issue.repository, {
|
|
1979
|
+
projectId: tenant.projectId,
|
|
1980
|
+
workspaceKey,
|
|
1981
|
+
issueSubjectId,
|
|
1982
|
+
issueIdentifier: issue.identifier,
|
|
1983
|
+
workspacePath: issueWorkspacePath,
|
|
1984
|
+
repositoryPath: repositoryDirectory
|
|
1985
|
+
});
|
|
1986
|
+
if (afterCreateResult && afterCreateResult.outcome !== "success" && afterCreateResult.outcome !== "skipped") {
|
|
1987
|
+
await this.store.appendRunEvent(runId, {
|
|
1988
|
+
at: now.toISOString(),
|
|
1989
|
+
event: "hook-failed",
|
|
1990
|
+
projectId: tenant.projectId,
|
|
1991
|
+
hook: "after_create",
|
|
1992
|
+
error: afterCreateResult.error ?? null
|
|
1993
|
+
});
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
const workflow = await this.loadProjectWorkflow(tenant, issue.repository);
|
|
1997
|
+
if (!isUsableWorkflowResolution(workflow)) {
|
|
1998
|
+
throw new Error(workflow.validationError ?? "Invalid repository WORKFLOW.md");
|
|
1999
|
+
}
|
|
2000
|
+
const allProjectRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
2001
|
+
const issueBudgetSnapshot = resolveIssueBudgetSnapshot(allProjectRuns, issue.id);
|
|
2002
|
+
const promptVariables = buildPromptVariables(issue, {
|
|
2003
|
+
attempt: null
|
|
2004
|
+
// first execution
|
|
2005
|
+
});
|
|
2006
|
+
const renderedPrompt = renderPrompt(workflow.promptTemplate, promptVariables);
|
|
2007
|
+
await this.runHook("before_run", tenant, repositoryDirectory, issue.repository, {
|
|
2008
|
+
projectId: tenant.projectId,
|
|
2009
|
+
workspaceKey,
|
|
2010
|
+
issueSubjectId,
|
|
2011
|
+
issueIdentifier: issue.identifier,
|
|
2012
|
+
workspacePath: issueWorkspacePath,
|
|
2013
|
+
repositoryPath: repositoryDirectory,
|
|
2014
|
+
runId,
|
|
2015
|
+
state: issue.state
|
|
2016
|
+
});
|
|
2017
|
+
mkdirSync(runDir, { recursive: true });
|
|
2018
|
+
const workerLogStream = (this.dependencies.createWriteStreamImpl ?? createWriteStream)(join3(runDir, "worker.log"), {
|
|
2019
|
+
flags: "a"
|
|
2020
|
+
});
|
|
2021
|
+
let workerLogAvailable = true;
|
|
2022
|
+
let workerExited = false;
|
|
2023
|
+
let workerStderrFinalizing = false;
|
|
2024
|
+
let workerLogBackpressured = false;
|
|
2025
|
+
const resumeWorkerStderr = () => {
|
|
2026
|
+
if (!workerLogBackpressured) {
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
workerLogBackpressured = false;
|
|
2030
|
+
child.stderr?.resume?.();
|
|
2031
|
+
};
|
|
2032
|
+
const markWorkerLogUnavailable = (error) => {
|
|
2033
|
+
resumeWorkerStderr();
|
|
2034
|
+
if (!workerLogAvailable) {
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
workerLogAvailable = false;
|
|
2038
|
+
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2039
|
+
this.writeStderr(`[orchestrator] failed to write worker log for ${runId}: ${message}`);
|
|
2040
|
+
};
|
|
2041
|
+
const child = (this.dependencies.spawnImpl ?? spawn2)("bash", ["-lc", resolveWorkerCommand()], {
|
|
2042
|
+
cwd: process.cwd(),
|
|
2043
|
+
env: this.buildProjectExecutionEnv(tenant.projectId, {
|
|
2044
|
+
GITHUB_GRAPHQL_TOKEN: process.env.GITHUB_GRAPHQL_TOKEN ?? "",
|
|
2045
|
+
CODEX_PROJECT_ID: tenant.projectId,
|
|
2046
|
+
PROJECT_ID: tenant.projectId,
|
|
2047
|
+
WORKING_DIRECTORY: repositoryDirectory,
|
|
2048
|
+
WORKSPACE_RUNTIME_DIR: workspaceRuntimeDir,
|
|
2049
|
+
SYMPHONY_RUN_ID: runId,
|
|
2050
|
+
SYMPHONY_ISSUE_STATE: issue.state,
|
|
2051
|
+
SYMPHONY_ISSUE_ID: issue.id,
|
|
2052
|
+
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
2053
|
+
SYMPHONY_ISSUE_TITLE: issue.title,
|
|
2054
|
+
SYMPHONY_ISSUE_SUBJECT_ID: issueSubjectId,
|
|
2055
|
+
SYMPHONY_ISSUE_WORKSPACE_KEY: workspaceKey,
|
|
2056
|
+
SYMPHONY_TRACKER_ADAPTER: issue.tracker.adapter,
|
|
2057
|
+
SYMPHONY_TRACKER_BINDING_ID: issue.tracker.bindingId,
|
|
2058
|
+
SYMPHONY_TRACKER_ITEM_ID: issue.tracker.itemId,
|
|
2059
|
+
TARGET_REPOSITORY_CLONE_URL: issue.repository.cloneUrl,
|
|
2060
|
+
TARGET_REPOSITORY_OWNER: issue.repository.owner,
|
|
2061
|
+
TARGET_REPOSITORY_NAME: issue.repository.name,
|
|
2062
|
+
TARGET_REPOSITORY_URL: issue.repository.url,
|
|
2063
|
+
...trackerAdapter.buildWorkerEnvironment(tenant, issue),
|
|
2064
|
+
SYMPHONY_RENDERED_PROMPT: renderedPrompt,
|
|
2065
|
+
SYMPHONY_WORKFLOW_PATH: workflow.workflowPath ?? "",
|
|
2066
|
+
SYMPHONY_AGENT_COMMAND: workflow.workflow.codex.command,
|
|
2067
|
+
SYMPHONY_APPROVAL_POLICY: workflow.workflow.codex.approvalPolicy ?? "",
|
|
2068
|
+
SYMPHONY_THREAD_SANDBOX: workflow.workflow.codex.threadSandbox ?? "",
|
|
2069
|
+
SYMPHONY_TURN_SANDBOX_POLICY: workflow.workflow.codex.turnSandboxPolicy ?? "",
|
|
2070
|
+
SYMPHONY_MAX_TURNS: String(workflow.workflow.agent.maxTurns),
|
|
2071
|
+
SYMPHONY_GLOBAL_MAX_TURNS: process.env.SYMPHONY_GLOBAL_MAX_TURNS ?? "",
|
|
2072
|
+
SYMPHONY_MAX_TOKENS: process.env.SYMPHONY_MAX_TOKENS ?? "",
|
|
2073
|
+
SYMPHONY_MAX_NONPRODUCTIVE_TURNS: process.env.SYMPHONY_MAX_NONPRODUCTIVE_TURNS ?? String(DEFAULT_MAX_NONPRODUCTIVE_TURNS),
|
|
2074
|
+
SYMPHONY_SESSION_TIMEOUT_MS: process.env.SYMPHONY_SESSION_TIMEOUT_MS ?? "",
|
|
2075
|
+
SYMPHONY_RESUME_THREAD_ID: resumeContext?.threadId ?? "",
|
|
2076
|
+
SYMPHONY_CUMULATIVE_TURN_COUNT: String(Math.max(0, resumeContext?.cumulativeTurnCount ?? issueBudgetSnapshot.cumulativeTurnCount)),
|
|
2077
|
+
SYMPHONY_CUMULATIVE_INPUT_TOKENS: String(issueBudgetSnapshot.tokenUsage.inputTokens),
|
|
2078
|
+
SYMPHONY_CUMULATIVE_OUTPUT_TOKENS: String(issueBudgetSnapshot.tokenUsage.outputTokens),
|
|
2079
|
+
SYMPHONY_CUMULATIVE_TOTAL_TOKENS: String(issueBudgetSnapshot.tokenUsage.totalTokens),
|
|
2080
|
+
SYMPHONY_LAST_TURN_SUMMARY: resumeContext?.lastTurnSummary ?? "",
|
|
2081
|
+
SYMPHONY_SESSION_STARTED_AT: issueBudgetSnapshot.sessionStartedAt ?? "",
|
|
2082
|
+
SYMPHONY_READ_TIMEOUT_MS: String(workflow.workflow.codex.readTimeoutMs),
|
|
2083
|
+
SYMPHONY_TURN_TIMEOUT_MS: String(workflow.workflow.codex.turnTimeoutMs)
|
|
2084
|
+
}),
|
|
2085
|
+
detached: true,
|
|
2086
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
2087
|
+
});
|
|
2088
|
+
const handleWorkerStderrChunk = (chunk) => {
|
|
2089
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
2090
|
+
if (workerLogAvailable) {
|
|
2091
|
+
try {
|
|
2092
|
+
if (!workerLogStream.write(buffer)) {
|
|
2093
|
+
workerLogBackpressured = true;
|
|
2094
|
+
child.stderr?.pause?.();
|
|
2095
|
+
}
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
markWorkerLogUnavailable(error);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
this.consumeWorkerStderrChunk(runId, buffer);
|
|
2101
|
+
};
|
|
2102
|
+
const drainWorkerStderr = () => {
|
|
2103
|
+
const stderr = child.stderr;
|
|
2104
|
+
if (!stderr || typeof stderr.read !== "function") {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
let chunk;
|
|
2108
|
+
while ((chunk = stderr.read()) !== null) {
|
|
2109
|
+
handleWorkerStderrChunk(chunk);
|
|
2110
|
+
}
|
|
2111
|
+
};
|
|
2112
|
+
const completeWorkerStderrFinalization = (code, signal) => {
|
|
2113
|
+
if (workerExited) {
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
workerExited = true;
|
|
2117
|
+
workerStderrFinalizing = false;
|
|
2118
|
+
child.stderr?.removeListener("data", handleWorkerStderrChunk);
|
|
2119
|
+
this.flushWorkerStderrBuffer(runId);
|
|
2120
|
+
workerLogStream.end();
|
|
2121
|
+
if (child.pid) {
|
|
2122
|
+
this.retireWorkerPid(child.pid);
|
|
2123
|
+
}
|
|
2124
|
+
this.logVerbose(`[worker-exited] ${runId} (code=${code ?? "null"}, signal=${signal ?? "null"})`);
|
|
2125
|
+
};
|
|
2126
|
+
const finalizeWorkerStderr = (code, signal) => {
|
|
2127
|
+
if (workerExited || workerStderrFinalizing) {
|
|
2128
|
+
return;
|
|
2129
|
+
}
|
|
2130
|
+
workerStderrFinalizing = true;
|
|
2131
|
+
const stderr = child.stderr;
|
|
2132
|
+
const finish = () => {
|
|
2133
|
+
stderr?.removeListener("end", finish);
|
|
2134
|
+
stderr?.removeListener("close", finish);
|
|
2135
|
+
drainWorkerStderr();
|
|
2136
|
+
completeWorkerStderrFinalization(code, signal);
|
|
2137
|
+
};
|
|
2138
|
+
resumeWorkerStderr();
|
|
2139
|
+
drainWorkerStderr();
|
|
2140
|
+
if (!stderr) {
|
|
2141
|
+
completeWorkerStderrFinalization(code, signal);
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
if (stderr.readableEnded || stderr.readable === false) {
|
|
2145
|
+
finish();
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
stderr.once("end", finish);
|
|
2149
|
+
stderr.once("close", finish);
|
|
2150
|
+
};
|
|
2151
|
+
workerLogStream.on("error", (error) => {
|
|
2152
|
+
markWorkerLogUnavailable(error);
|
|
2153
|
+
});
|
|
2154
|
+
workerLogStream.on("drain", () => {
|
|
2155
|
+
resumeWorkerStderr();
|
|
2156
|
+
});
|
|
2157
|
+
child.stderr?.on("data", handleWorkerStderrChunk);
|
|
2158
|
+
if (child.pid) {
|
|
2159
|
+
this.activeWorkerPids.add(child.pid);
|
|
2160
|
+
this.logVerbose(`[worker-started] ${runId} (pid=${child.pid})`);
|
|
2161
|
+
}
|
|
2162
|
+
child.on?.("error", (error) => {
|
|
2163
|
+
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2164
|
+
this.writeStderr(`[orchestrator] worker process error for ${runId}: ${message}`);
|
|
2165
|
+
finalizeWorkerStderr(null, null);
|
|
2166
|
+
});
|
|
2167
|
+
child.on?.("close", (code, signal) => {
|
|
2168
|
+
finalizeWorkerStderr(code, signal);
|
|
2169
|
+
});
|
|
2170
|
+
child.unref();
|
|
2171
|
+
return {
|
|
2172
|
+
runId,
|
|
2173
|
+
projectId: tenant.projectId,
|
|
2174
|
+
projectSlug: tenant.slug,
|
|
2175
|
+
issueId: issue.id,
|
|
2176
|
+
issueSubjectId,
|
|
2177
|
+
issueIdentifier: issue.identifier,
|
|
2178
|
+
issueTitle: issue.title,
|
|
2179
|
+
issueState: issue.state,
|
|
2180
|
+
repository: issue.repository,
|
|
2181
|
+
status: "running",
|
|
2182
|
+
attempt: 1,
|
|
2183
|
+
processId: child.pid ?? null,
|
|
2184
|
+
port: null,
|
|
2185
|
+
workingDirectory: repositoryDirectory,
|
|
2186
|
+
issueWorkspaceKey: workspaceKey,
|
|
2187
|
+
workspaceRuntimeDir,
|
|
2188
|
+
workflowPath: workflow.workflowPath,
|
|
2189
|
+
retryKind: null,
|
|
2190
|
+
threadId: null,
|
|
2191
|
+
cumulativeTurnCount: 0,
|
|
2192
|
+
lastTurnSummary: null,
|
|
2193
|
+
createdAt: now.toISOString(),
|
|
2194
|
+
updatedAt: now.toISOString(),
|
|
2195
|
+
startedAt: now.toISOString(),
|
|
2196
|
+
completedAt: null,
|
|
2197
|
+
lastError: null,
|
|
2198
|
+
nextRetryAt: null,
|
|
2199
|
+
runPhase: "preparing_workspace",
|
|
2200
|
+
rateLimits: issue.rateLimits ?? null
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
async syncActiveRunIssueStates(tenant, trackerAdapter, activeRuns, now) {
|
|
2204
|
+
const activeIssueIds = [...new Set(activeRuns.map((run) => run.issueId))];
|
|
2205
|
+
if (activeIssueIds.length === 0) {
|
|
2206
|
+
return {
|
|
2207
|
+
runs: activeRuns,
|
|
2208
|
+
issuesByIdentifier: /* @__PURE__ */ new Map()
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
const issues = await trackerAdapter.fetchIssueStatesByIds(tenant, activeIssueIds, {
|
|
2212
|
+
fetchImpl: this.dependencies.fetchImpl
|
|
2213
|
+
});
|
|
2214
|
+
const issuesByIdentifier = new Map(issues.map((issue) => [issue.identifier, issue]));
|
|
2215
|
+
const issueStateByIdentifier = new Map(issues.map((issue) => [issue.identifier, issue.state]));
|
|
2216
|
+
const syncedRuns = [];
|
|
2217
|
+
for (const run of activeRuns) {
|
|
2218
|
+
const currentTrackerState = issueStateByIdentifier.get(run.issueIdentifier);
|
|
2219
|
+
if (!currentTrackerState || currentTrackerState === run.issueState) {
|
|
2220
|
+
syncedRuns.push(run);
|
|
2221
|
+
continue;
|
|
2222
|
+
}
|
|
2223
|
+
const updatedRun = {
|
|
2224
|
+
...run,
|
|
2225
|
+
issueState: currentTrackerState,
|
|
2226
|
+
updatedAt: now.toISOString()
|
|
2227
|
+
};
|
|
2228
|
+
await this.store.saveRun(updatedRun);
|
|
2229
|
+
syncedRuns.push(updatedRun);
|
|
2230
|
+
}
|
|
2231
|
+
return {
|
|
2232
|
+
runs: syncedRuns,
|
|
2233
|
+
issuesByIdentifier
|
|
2234
|
+
};
|
|
2235
|
+
}
|
|
2236
|
+
async reconcileRun(tenant, run, issueRecords) {
|
|
2237
|
+
const now = this.now();
|
|
2238
|
+
if (run.processId && this.isProcessRunning(run.processId)) {
|
|
2239
|
+
const retryPolicy = await this.loadRetryPolicy(tenant, run.repository);
|
|
2240
|
+
const configuredStallTimeoutMs = retryPolicy?.stallTimeoutMs ?? null;
|
|
2241
|
+
const lastActivityAtMs = parseTimestampMs(run.lastEventAt ?? run.startedAt);
|
|
2242
|
+
const startedAtMs = parseTimestampMs(run.startedAt);
|
|
2243
|
+
const elapsedSinceLastActivityMs = lastActivityAtMs === null ? null : now.getTime() - lastActivityAtMs;
|
|
2244
|
+
const runningSinceMs = startedAtMs === null ? null : now.getTime() - startedAtMs;
|
|
2245
|
+
const isStalledByWorkflowTimeout = configuredStallTimeoutMs !== null && configuredStallTimeoutMs > 0 && elapsedSinceLastActivityMs !== null && elapsedSinceLastActivityMs > configuredStallTimeoutMs;
|
|
2246
|
+
const isStalledByFallbackTimeout = runningSinceMs !== null && runningSinceMs > STUCK_WORKER_TIMEOUT_MS;
|
|
2247
|
+
if (isStalledByWorkflowTimeout || isStalledByFallbackTimeout) {
|
|
2248
|
+
const elapsedMs = isStalledByWorkflowTimeout ? elapsedSinceLastActivityMs : runningSinceMs;
|
|
2249
|
+
const timeoutMs = isStalledByWorkflowTimeout ? configuredStallTimeoutMs : STUCK_WORKER_TIMEOUT_MS;
|
|
2250
|
+
const elapsedSeconds = Math.round((elapsedMs ?? 0) / 1e3);
|
|
2251
|
+
const timeoutSeconds = Math.round((timeoutMs ?? 0) / 1e3);
|
|
2252
|
+
if (this.isVerboseLoggingEnabled()) {
|
|
2253
|
+
this.writeStderr(`[stall-detected] ${run.runId} (elapsed=${elapsedSeconds}s > ${timeoutSeconds}s)`);
|
|
2254
|
+
} else {
|
|
2255
|
+
this.writeStderr(`[orchestrator] stuck worker detected for ${run.runId} (elapsed ${elapsedSeconds}s > ${timeoutSeconds}s) \u2014 sending SIGTERM`);
|
|
2256
|
+
}
|
|
2257
|
+
this.sendSignal(run.processId, "SIGTERM");
|
|
2258
|
+
} else {
|
|
2259
|
+
const runningRecord = {
|
|
2260
|
+
...run,
|
|
2261
|
+
status: "running",
|
|
2262
|
+
updatedAt: now.toISOString()
|
|
2263
|
+
};
|
|
2264
|
+
await this.store.saveRun(runningRecord);
|
|
2265
|
+
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2266
|
+
issueId: run.issueId,
|
|
2267
|
+
identifier: run.issueIdentifier,
|
|
2268
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey({
|
|
2269
|
+
projectId: tenant.projectId,
|
|
2270
|
+
adapter: tenant.tracker.adapter,
|
|
2271
|
+
issueSubjectId: run.issueSubjectId
|
|
2272
|
+
}, run.issueIdentifier),
|
|
2273
|
+
state: "running",
|
|
2274
|
+
currentRunId: run.runId,
|
|
2275
|
+
retryEntry: null,
|
|
2276
|
+
updatedAt: now.toISOString()
|
|
2277
|
+
});
|
|
2278
|
+
return {
|
|
2279
|
+
issueRecords,
|
|
2280
|
+
recovered: false
|
|
2281
|
+
};
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
if (run.processId) {
|
|
2285
|
+
this.retireWorkerPid(run.processId);
|
|
2286
|
+
}
|
|
2287
|
+
const workerInfo = await this.fetchWorkerRunInfo(run);
|
|
2288
|
+
const runWithTokens = {
|
|
2289
|
+
...run,
|
|
2290
|
+
runtimeSession: buildRuntimeSession(run.runtimeSession, workerInfo.sessionId, workerInfo.threadId, run.status === "running" ? "failed" : run.runtimeSession?.status ?? null, run.runtimeSession?.startedAt ?? run.startedAt ?? now.toISOString(), now.toISOString(), workerInfo.exitClassification),
|
|
2291
|
+
threadId: workerInfo.threadId ?? run.threadId ?? null,
|
|
2292
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(run, workerInfo.turnCount ?? null),
|
|
2293
|
+
tokenUsage: workerInfo.tokenUsage ?? run.tokenUsage,
|
|
2294
|
+
lastEvent: workerInfo.lastEvent ?? run.lastEvent,
|
|
2295
|
+
lastTurnSummary: resolveLastTurnSummary(run.lastTurnSummary, resolveLastTurnSummaryCandidate(workerInfo.lastEvent, workerInfo.lastError)),
|
|
2296
|
+
lastEventAt: workerInfo.lastEventAt ?? run.lastEventAt ?? void 0,
|
|
2297
|
+
lastEventAtSource: workerInfo.lastEventAtSource ?? run.lastEventAtSource ?? void 0,
|
|
2298
|
+
executionPhase: workerInfo.executionPhase ?? run.executionPhase ?? null,
|
|
2299
|
+
runPhase: workerInfo.runPhase ?? run.runPhase ?? null,
|
|
2300
|
+
rateLimits: workerInfo.rateLimits ?? run.rateLimits ?? null
|
|
2301
|
+
};
|
|
2302
|
+
const workerSessionId = workerInfo.sessionId;
|
|
2303
|
+
if (workerInfo.lastError) {
|
|
2304
|
+
await this.store.appendRunEvent(run.runId, {
|
|
2305
|
+
at: now.toISOString(),
|
|
2306
|
+
event: "worker-error",
|
|
2307
|
+
projectId: run.projectId,
|
|
2308
|
+
runId: run.runId,
|
|
2309
|
+
issueIdentifier: run.issueIdentifier,
|
|
2310
|
+
error: workerInfo.lastError,
|
|
2311
|
+
attempt: run.attempt
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
if (run.status === "retrying" && run.nextRetryAt) {
|
|
2315
|
+
if (new Date(run.nextRetryAt).getTime() > now.getTime()) {
|
|
2316
|
+
return {
|
|
2317
|
+
issueRecords,
|
|
2318
|
+
recovered: false
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
if (await this.resolveRetryRestartAction(tenant, run) === "release") {
|
|
2322
|
+
return this.releaseRetryingRun(runWithTokens, issueRecords, now);
|
|
2323
|
+
}
|
|
2324
|
+
return this.restartRun(tenant, run, issueRecords, now, workerSessionId);
|
|
2325
|
+
}
|
|
2326
|
+
if (workerInfo.exitClassification === "budget-exceeded" || workerInfo.exitClassification === "convergence-detected") {
|
|
2327
|
+
const completedRun = {
|
|
2328
|
+
...runWithTokens,
|
|
2329
|
+
status: workerInfo.exitClassification === "budget-exceeded" ? "succeeded" : "failed",
|
|
2330
|
+
processId: null,
|
|
2331
|
+
updatedAt: now.toISOString(),
|
|
2332
|
+
completedAt: now.toISOString(),
|
|
2333
|
+
nextRetryAt: null,
|
|
2334
|
+
retryKind: null,
|
|
2335
|
+
lastError: workerInfo.exitClassification === "budget-exceeded" ? null : runWithTokens.lastError,
|
|
2336
|
+
runPhase: runWithTokens.runPhase ?? (workerInfo.exitClassification === "budget-exceeded" ? "succeeded" : "failed")
|
|
2337
|
+
};
|
|
2338
|
+
await this.store.saveRun(completedRun);
|
|
2339
|
+
this.logVerbose(`[run-completed] ${completedRun.runId} status=${completedRun.status}`);
|
|
2340
|
+
return {
|
|
2341
|
+
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
2342
|
+
recovered: false
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
if (run.issueWorkspaceKey) {
|
|
2346
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(this.store.projectDir(tenant.projectId), run.issueWorkspaceKey);
|
|
2347
|
+
await this.runHook("after_run", tenant, run.workingDirectory, run.repository, {
|
|
2348
|
+
projectId: run.projectId,
|
|
2349
|
+
workspaceKey: run.issueWorkspaceKey,
|
|
2350
|
+
issueSubjectId: run.issueSubjectId,
|
|
2351
|
+
issueIdentifier: run.issueIdentifier,
|
|
2352
|
+
workspacePath: issueWorkspacePath,
|
|
2353
|
+
repositoryPath: run.workingDirectory,
|
|
2354
|
+
runId: run.runId,
|
|
2355
|
+
state: run.issueState
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
const retryKind = await this.classifyRetryKind(tenant, run);
|
|
2359
|
+
let nextRetryAt;
|
|
2360
|
+
if (retryKind === "continuation") {
|
|
2361
|
+
nextRetryAt = new Date(now.getTime() + CONTINUATION_RETRY_DELAY_MS).toISOString();
|
|
2362
|
+
} else {
|
|
2363
|
+
const retryOptions = await this.loadRetryPolicy(tenant, run.repository);
|
|
2364
|
+
const backoffMs = this.dependencies.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
2365
|
+
nextRetryAt = (retryOptions ? scheduleRetryAt(now, run.attempt + 1, retryOptions) : new Date(now.getTime() + backoffMs)).toISOString();
|
|
2366
|
+
}
|
|
2367
|
+
const retryRecord = {
|
|
2368
|
+
...runWithTokens,
|
|
2369
|
+
status: "retrying",
|
|
2370
|
+
attempt: runWithTokens.attempt + 1,
|
|
2371
|
+
processId: null,
|
|
2372
|
+
updatedAt: now.toISOString(),
|
|
2373
|
+
nextRetryAt,
|
|
2374
|
+
retryKind,
|
|
2375
|
+
threadId: runWithTokens.threadId ?? runWithTokens.runtimeSession?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2376
|
+
cumulativeTurnCount: runWithTokens.cumulativeTurnCount ?? run.cumulativeTurnCount ?? 0,
|
|
2377
|
+
lastTurnSummary: runWithTokens.lastTurnSummary ?? run.lastTurnSummary ?? null,
|
|
2378
|
+
runPhase: runWithTokens.runPhase ?? "failed",
|
|
2379
|
+
lastError: retryKind === "continuation" ? null : "Worker process exited unexpectedly."
|
|
2380
|
+
};
|
|
2381
|
+
await this.store.saveRun(retryRecord);
|
|
2382
|
+
this.logVerbose(`[retry-scheduled] ${retryRecord.runId} kind=${retryKind} attempt=${retryRecord.attempt} nextAt=${nextRetryAt}`);
|
|
2383
|
+
this.logVerbose(`[run-completed] ${retryRecord.runId} status=${retryRecord.status}`);
|
|
2384
|
+
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2385
|
+
issueId: run.issueId,
|
|
2386
|
+
identifier: run.issueIdentifier,
|
|
2387
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey({
|
|
2388
|
+
projectId: tenant.projectId,
|
|
2389
|
+
adapter: tenant.tracker.adapter,
|
|
2390
|
+
issueSubjectId: run.issueSubjectId
|
|
2391
|
+
}, run.issueIdentifier),
|
|
2392
|
+
state: "retry_queued",
|
|
2393
|
+
completedOnce: retryKind === "continuation" ? true : void 0,
|
|
2394
|
+
currentRunId: run.runId,
|
|
2395
|
+
retryEntry: {
|
|
2396
|
+
attempt: retryRecord.attempt,
|
|
2397
|
+
dueAt: nextRetryAt,
|
|
2398
|
+
error: retryRecord.lastError
|
|
2399
|
+
},
|
|
2400
|
+
updatedAt: now.toISOString()
|
|
2401
|
+
});
|
|
2402
|
+
return {
|
|
2403
|
+
issueRecords,
|
|
2404
|
+
recovered: false
|
|
2405
|
+
};
|
|
2406
|
+
}
|
|
2407
|
+
now() {
|
|
2408
|
+
return this.dependencies.now?.() ?? /* @__PURE__ */ new Date();
|
|
2409
|
+
}
|
|
2410
|
+
isVerboseLoggingEnabled() {
|
|
2411
|
+
return this.dependencies.logLevel === "verbose";
|
|
2412
|
+
}
|
|
2413
|
+
writeStderr(message) {
|
|
2414
|
+
(this.dependencies.stderr ?? process.stderr).write(`${message}
|
|
2415
|
+
`);
|
|
2416
|
+
}
|
|
2417
|
+
consumeWorkerStderrChunk(runId, chunk) {
|
|
2418
|
+
let decoder = this.workerStderrDecoders.get(runId);
|
|
2419
|
+
if (!decoder) {
|
|
2420
|
+
decoder = new StringDecoder("utf8");
|
|
2421
|
+
this.workerStderrDecoders.set(runId, decoder);
|
|
2422
|
+
}
|
|
2423
|
+
const nextBuffer = (this.workerStderrBuffers.get(runId) ?? "") + decoder.write(chunk);
|
|
2424
|
+
const lines = nextBuffer.split("\n");
|
|
2425
|
+
this.workerStderrBuffers.set(runId, lines.pop() ?? "");
|
|
2426
|
+
for (const line of lines) {
|
|
2427
|
+
this.consumeWorkerStderrLine(runId, line);
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
flushWorkerStderrBuffer(runId) {
|
|
2431
|
+
const decoder = this.workerStderrDecoders.get(runId);
|
|
2432
|
+
const remainder = (this.workerStderrBuffers.get(runId) ?? "") + (decoder?.end() ?? "");
|
|
2433
|
+
this.workerStderrBuffers.delete(runId);
|
|
2434
|
+
this.workerStderrDecoders.delete(runId);
|
|
2435
|
+
if (remainder && remainder.trim()) {
|
|
2436
|
+
this.consumeWorkerStderrLine(runId, remainder);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
consumeWorkerStderrLine(runId, line) {
|
|
2440
|
+
const trimmed = line.trim();
|
|
2441
|
+
if (!trimmed || !trimmed.startsWith("{")) {
|
|
2442
|
+
return;
|
|
2443
|
+
}
|
|
2444
|
+
try {
|
|
2445
|
+
const parsed = JSON.parse(trimmed);
|
|
2446
|
+
if (!isOrchestratorChannelEvent(parsed)) {
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
void this.runSerialized(() => this.applyWorkerChannelEvent(runId, parsed)).catch((error) => {
|
|
2450
|
+
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
2451
|
+
this.writeStderr(`[orchestrator] failed to apply worker channel event for ${runId}: ${message}`);
|
|
2452
|
+
});
|
|
2453
|
+
} catch {
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
async applyWorkerChannelEvent(runId, event) {
|
|
2457
|
+
const run = await this.store.loadRun(runId, this.projectConfig.projectId);
|
|
2458
|
+
if (!run || !canApplyWorkerChannelUpdate(run.status) || run.issueId !== event.issueId) {
|
|
2459
|
+
return;
|
|
2460
|
+
}
|
|
2461
|
+
if (event.type === "heartbeat") {
|
|
2462
|
+
const nowIso2 = this.now().toISOString();
|
|
2463
|
+
const persistedLastEventAt = event.lastEventAt ?? run.lastEventAt ?? null;
|
|
2464
|
+
await this.store.saveRun({
|
|
2465
|
+
...run,
|
|
2466
|
+
updatedAt: nowIso2,
|
|
2467
|
+
lastEvent: "heartbeat",
|
|
2468
|
+
lastTurnSummary: resolveLastTurnSummary(run.lastTurnSummary, event.lastError),
|
|
2469
|
+
lastEventAt: persistedLastEventAt,
|
|
2470
|
+
lastEventAtSource: event.lastEventAt != null ? "event-channel" : run.lastEventAtSource ?? null,
|
|
2471
|
+
tokenUsage: event.tokenUsage,
|
|
2472
|
+
rateLimits: event.rateLimits,
|
|
2473
|
+
runtimeSession: buildRuntimeSession(run.runtimeSession, resolveChannelSessionId(event.sessionInfo), event.sessionInfo?.threadId ?? null, "active", run.startedAt ?? run.runtimeSession?.startedAt ?? nowIso2, nowIso2, event.sessionInfo?.exitClassification ?? null),
|
|
2474
|
+
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2475
|
+
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
2476
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(run, event.sessionInfo?.turnCount ?? null),
|
|
2477
|
+
executionPhase: event.executionPhase ?? run.executionPhase,
|
|
2478
|
+
runPhase: event.runPhase ?? run.runPhase,
|
|
2479
|
+
lastError: event.lastError
|
|
2480
|
+
});
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
if (event.type === "turn_started") {
|
|
2484
|
+
await this.store.appendRunEvent(runId, {
|
|
2485
|
+
at: event.startedAt,
|
|
2486
|
+
event: "turn_started",
|
|
2487
|
+
projectId: run.projectId,
|
|
2488
|
+
issueIdentifier: run.issueIdentifier,
|
|
2489
|
+
issueId: run.issueId,
|
|
2490
|
+
sessionId: event.sessionId,
|
|
2491
|
+
threadId: event.threadId,
|
|
2492
|
+
turnId: event.turnId,
|
|
2493
|
+
turnCount: event.turnCount
|
|
2494
|
+
});
|
|
2495
|
+
return;
|
|
2496
|
+
}
|
|
2497
|
+
if (event.type === "turn_completed") {
|
|
2498
|
+
await this.store.appendRunEvent(runId, {
|
|
2499
|
+
at: event.completedAt,
|
|
2500
|
+
event: "turn_completed",
|
|
2501
|
+
projectId: run.projectId,
|
|
2502
|
+
issueIdentifier: run.issueIdentifier,
|
|
2503
|
+
issueId: run.issueId,
|
|
2504
|
+
sessionId: event.sessionId,
|
|
2505
|
+
threadId: event.threadId,
|
|
2506
|
+
turnId: event.turnId,
|
|
2507
|
+
turnCount: event.turnCount,
|
|
2508
|
+
startedAt: event.startedAt,
|
|
2509
|
+
durationMs: event.durationMs,
|
|
2510
|
+
tokenUsage: event.tokenUsage
|
|
2511
|
+
});
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
if (event.type === "turn_failed") {
|
|
2515
|
+
await this.store.appendRunEvent(runId, {
|
|
2516
|
+
at: event.failedAt,
|
|
2517
|
+
event: "turn_failed",
|
|
2518
|
+
projectId: run.projectId,
|
|
2519
|
+
issueIdentifier: run.issueIdentifier,
|
|
2520
|
+
issueId: run.issueId,
|
|
2521
|
+
sessionId: event.sessionId,
|
|
2522
|
+
threadId: event.threadId,
|
|
2523
|
+
turnId: event.turnId,
|
|
2524
|
+
turnCount: event.turnCount,
|
|
2525
|
+
startedAt: event.startedAt,
|
|
2526
|
+
durationMs: event.durationMs,
|
|
2527
|
+
tokenUsage: event.tokenUsage,
|
|
2528
|
+
error: event.error
|
|
2529
|
+
});
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
const nowIso = this.now().toISOString();
|
|
2533
|
+
await this.store.saveRun({
|
|
2534
|
+
...run,
|
|
2535
|
+
updatedAt: nowIso,
|
|
2536
|
+
lastEvent: event.event ?? run.lastEvent ?? null,
|
|
2537
|
+
lastTurnSummary: resolveLastTurnSummary(run.lastTurnSummary, resolveLastTurnSummaryCandidate(event.event, event.lastError)),
|
|
2538
|
+
lastEventAt: event.lastEventAt,
|
|
2539
|
+
lastEventAtSource: "event-channel",
|
|
2540
|
+
tokenUsage: event.tokenUsage ?? run.tokenUsage,
|
|
2541
|
+
rateLimits: event.rateLimits ?? run.rateLimits ?? null,
|
|
2542
|
+
runtimeSession: buildRuntimeSession(run.runtimeSession, resolveChannelSessionId(event.sessionInfo), event.sessionInfo?.threadId ?? run.runtimeSession?.threadId ?? null, "active", run.startedAt ?? run.runtimeSession?.startedAt ?? nowIso, nowIso, event.sessionInfo?.exitClassification ?? null),
|
|
2543
|
+
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2544
|
+
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
2545
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(run, event.sessionInfo?.turnCount ?? null),
|
|
2546
|
+
executionPhase: event.executionPhase ?? run.executionPhase ?? null,
|
|
2547
|
+
runPhase: event.runPhase ?? run.runPhase ?? null,
|
|
2548
|
+
lastError: event.lastError ?? run.lastError
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
logVerbose(message) {
|
|
2552
|
+
if (!this.isVerboseLoggingEnabled()) {
|
|
2553
|
+
return;
|
|
2554
|
+
}
|
|
2555
|
+
this.writeStderr(message);
|
|
2556
|
+
}
|
|
2557
|
+
async waitForNextPoll() {
|
|
2558
|
+
if (this.consumePendingReconcileRequest()) {
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
const customWait = this.dependencies.waitImpl;
|
|
2562
|
+
const pollIntervalMs = this.getEffectivePollIntervalMs();
|
|
2563
|
+
const waitPromise = this.createPendingSleepPromise();
|
|
2564
|
+
try {
|
|
2565
|
+
if (customWait) {
|
|
2566
|
+
await Promise.race([customWait(pollIntervalMs), waitPromise]);
|
|
2567
|
+
} else {
|
|
2568
|
+
this.sleepTimer = setTimeout(() => {
|
|
2569
|
+
this.sleepResolver?.();
|
|
2570
|
+
}, pollIntervalMs);
|
|
2571
|
+
await waitPromise;
|
|
2572
|
+
}
|
|
2573
|
+
} finally {
|
|
2574
|
+
this.cancelPendingSleep();
|
|
2575
|
+
}
|
|
2576
|
+
this.consumePendingReconcileRequest();
|
|
2577
|
+
}
|
|
2578
|
+
cancelPendingSleep() {
|
|
2579
|
+
if (this.sleepTimer) {
|
|
2580
|
+
clearTimeout(this.sleepTimer);
|
|
2581
|
+
this.sleepTimer = null;
|
|
2582
|
+
}
|
|
2583
|
+
this.sleepResolver?.();
|
|
2584
|
+
this.sleepResolver = null;
|
|
2585
|
+
}
|
|
2586
|
+
createPendingSleepPromise() {
|
|
2587
|
+
return new Promise((resolve4) => {
|
|
2588
|
+
this.sleepResolver = () => {
|
|
2589
|
+
this.sleepResolver = null;
|
|
2590
|
+
this.sleepTimer = null;
|
|
2591
|
+
resolve4();
|
|
2592
|
+
};
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
consumePendingReconcileRequest() {
|
|
2596
|
+
if (!this.reconcileRequested) {
|
|
2597
|
+
return false;
|
|
2598
|
+
}
|
|
2599
|
+
this.reconcileRequested = false;
|
|
2600
|
+
return true;
|
|
2601
|
+
}
|
|
2602
|
+
/**
|
|
2603
|
+
* Classify whether a process exit should be treated as continuation retry
|
|
2604
|
+
* or failure retry. Continuation applies when the issue is still actionable
|
|
2605
|
+
* — the worker completed its session and the issue hasn't transitioned away.
|
|
2606
|
+
* Failure applies when we cannot confirm the issue is still actionable.
|
|
2607
|
+
*/
|
|
2608
|
+
async classifyRetryKind(tenant, run) {
|
|
2609
|
+
try {
|
|
2610
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
2611
|
+
const issues = await trackerAdapter.listIssues(tenant, {
|
|
2612
|
+
fetchImpl: this.dependencies.fetchImpl
|
|
2613
|
+
});
|
|
2614
|
+
const runIssue = issues.find((issue) => issue.identifier === run.issueIdentifier);
|
|
2615
|
+
if (!runIssue) {
|
|
2616
|
+
return "failure";
|
|
2617
|
+
}
|
|
2618
|
+
const resolution = await this.loadProjectWorkflow(tenant, run.repository);
|
|
2619
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
2620
|
+
return "failure";
|
|
2621
|
+
}
|
|
2622
|
+
return isStateActive(runIssue.state, resolution.lifecycle) ? "continuation" : "failure";
|
|
2623
|
+
} catch {
|
|
2624
|
+
return "failure";
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
async resolveRetryRestartAction(tenant, run) {
|
|
2628
|
+
try {
|
|
2629
|
+
if (isIssueBudgetExceeded(resolveIssueBudgetSnapshot((await this.store.loadAllRuns()).filter((candidate) => candidate.projectId === tenant.projectId), run.issueId), this.now())) {
|
|
2630
|
+
return "release";
|
|
2631
|
+
}
|
|
2632
|
+
const runIssue = await this.fetchTrackedIssueById(tenant, run.issueId);
|
|
2633
|
+
if (!runIssue) {
|
|
2634
|
+
return "release";
|
|
2635
|
+
}
|
|
2636
|
+
const resolution = await this.loadProjectWorkflow(tenant, run.repository);
|
|
2637
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
2638
|
+
return "restart";
|
|
2639
|
+
}
|
|
2640
|
+
return isStateActive(runIssue.state, resolution.lifecycle) ? "restart" : "release";
|
|
2641
|
+
} catch {
|
|
2642
|
+
return "restart";
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
async fetchTrackedIssueById(tenant, issueId) {
|
|
2646
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
2647
|
+
const issues = await trackerAdapter.fetchIssueStatesByIds(tenant, [issueId], {
|
|
2648
|
+
fetchImpl: this.dependencies.fetchImpl
|
|
2649
|
+
});
|
|
2650
|
+
return issues[0] ?? null;
|
|
2651
|
+
}
|
|
2652
|
+
async fetchWorkerRunInfo(run) {
|
|
2653
|
+
const latestRun = await this.store.loadRun(run.runId, run.projectId) ?? run;
|
|
2654
|
+
const persistedTokenUsage = await this.readPersistedWorkerTokenUsage(latestRun);
|
|
2655
|
+
return {
|
|
2656
|
+
tokenUsage: persistedTokenUsage,
|
|
2657
|
+
sessionId: latestRun.runtimeSession?.sessionId ?? null,
|
|
2658
|
+
threadId: latestRun.threadId ?? latestRun.runtimeSession?.threadId ?? null,
|
|
2659
|
+
turnCount: latestRun.turnCount ?? null,
|
|
2660
|
+
exitClassification: latestRun.runtimeSession?.exitClassification ?? null,
|
|
2661
|
+
lastError: latestRun.lastError ?? null,
|
|
2662
|
+
lastEvent: latestRun.lastEvent ?? null,
|
|
2663
|
+
lastEventAt: latestRun.lastEventAt ?? null,
|
|
2664
|
+
lastEventAtSource: latestRun.lastEventAtSource ?? null,
|
|
2665
|
+
executionPhase: latestRun.executionPhase ?? null,
|
|
2666
|
+
runPhase: latestRun.runPhase ?? null,
|
|
2667
|
+
rateLimits: latestRun.rateLimits ?? null
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2670
|
+
async readPersistedWorkerTokenUsage(run) {
|
|
2671
|
+
const artifactPaths = [
|
|
2672
|
+
join3(run.workspaceRuntimeDir, "token-usage.json"),
|
|
2673
|
+
join3(run.workspaceRuntimeDir, ".orchestrator", "runs", run.runId, "token-usage.json")
|
|
2674
|
+
];
|
|
2675
|
+
for (const artifactPath of artifactPaths) {
|
|
2676
|
+
try {
|
|
2677
|
+
const raw = await readFile3(artifactPath, "utf8");
|
|
2678
|
+
const tokenUsage = JSON.parse(raw);
|
|
2679
|
+
if (hasTokenUsage(tokenUsage)) {
|
|
2680
|
+
return tokenUsage;
|
|
2681
|
+
}
|
|
2682
|
+
} catch {
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
return null;
|
|
2687
|
+
}
|
|
2688
|
+
/**
|
|
2689
|
+
* Execute a workspace lifecycle hook using the workflow configuration
|
|
2690
|
+
* loaded from the repository. Returns the hook result or null if the
|
|
2691
|
+
* workflow could not be loaded.
|
|
2692
|
+
*/
|
|
2693
|
+
async runHook(kind, tenant, repositoryDirectory, repository, context, resolution) {
|
|
2694
|
+
try {
|
|
2695
|
+
const workflowResolution = resolution ?? await this.loadProjectWorkflow(tenant, repository);
|
|
2696
|
+
if (!isUsableWorkflowResolution(workflowResolution)) {
|
|
2697
|
+
return null;
|
|
2698
|
+
}
|
|
2699
|
+
const hookEnv = this.buildProjectExecutionEnv(tenant.projectId, buildHookEnv(context));
|
|
2700
|
+
return executeWorkspaceHook({
|
|
2701
|
+
kind,
|
|
2702
|
+
hooks: workflowResolution.workflow.hooks,
|
|
2703
|
+
repositoryPath: repositoryDirectory,
|
|
2704
|
+
env: hookEnv,
|
|
2705
|
+
timeoutMs: workflowResolution.workflow.hooks.timeoutMs
|
|
2706
|
+
});
|
|
2707
|
+
} catch {
|
|
2708
|
+
return null;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
readProjectEnv(projectId) {
|
|
2712
|
+
const envPath = join3(this.store.projectDir(projectId), ".env");
|
|
2713
|
+
try {
|
|
2714
|
+
return readEnvFile(envPath);
|
|
2715
|
+
} catch (error) {
|
|
2716
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred.";
|
|
2717
|
+
(this.dependencies.stderr ?? process.stderr).write(`[warn] Failed to load project env for ${projectId} from ${envPath}: ${message}
|
|
2718
|
+
`);
|
|
2719
|
+
return {};
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
buildProjectExecutionEnv(projectId, env) {
|
|
2723
|
+
const inheritedEnv = Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string"));
|
|
2724
|
+
const explicitEnv = Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
|
|
2725
|
+
return {
|
|
2726
|
+
...this.readProjectEnv(projectId),
|
|
2727
|
+
...inheritedEnv,
|
|
2728
|
+
...explicitEnv
|
|
2729
|
+
};
|
|
2730
|
+
}
|
|
2731
|
+
async restartRun(tenant, run, issueRecords, now, sessionId) {
|
|
2732
|
+
const supersededRecord = {
|
|
2733
|
+
...run,
|
|
2734
|
+
status: "failed",
|
|
2735
|
+
completedAt: now.toISOString(),
|
|
2736
|
+
updatedAt: now.toISOString(),
|
|
2737
|
+
lastError: "Superseded by recovered run."
|
|
2738
|
+
};
|
|
2739
|
+
await this.store.saveRun(supersededRecord);
|
|
2740
|
+
const issue = resolveTrackerAdapter2(tenant.tracker).reviveIssue(tenant, run);
|
|
2741
|
+
const restarted = await this.startRun(tenant, issue, {
|
|
2742
|
+
threadId: run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2743
|
+
cumulativeTurnCount: resolvePersistedCumulativeTurnCount(run),
|
|
2744
|
+
lastTurnSummary: run.lastTurnSummary ?? null
|
|
2745
|
+
});
|
|
2746
|
+
const recoveredRecord = {
|
|
2747
|
+
...restarted,
|
|
2748
|
+
attempt: run.attempt,
|
|
2749
|
+
retryKind: run.retryKind ?? "recovery",
|
|
2750
|
+
createdAt: run.createdAt,
|
|
2751
|
+
issueWorkspaceKey: run.issueWorkspaceKey,
|
|
2752
|
+
threadId: run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
2753
|
+
cumulativeTurnCount: resolvePersistedCumulativeTurnCount(run),
|
|
2754
|
+
lastTurnSummary: run.lastTurnSummary ?? null,
|
|
2755
|
+
turnCount: 0
|
|
2756
|
+
};
|
|
2757
|
+
await this.store.saveRun(recoveredRecord);
|
|
2758
|
+
await this.store.appendRunEvent(run.runId, {
|
|
2759
|
+
at: now.toISOString(),
|
|
2760
|
+
event: "run-recovered",
|
|
2761
|
+
projectId: run.projectId,
|
|
2762
|
+
issueIdentifier: run.issueIdentifier,
|
|
2763
|
+
issueId: run.issueId,
|
|
2764
|
+
sessionId: sessionId ?? void 0
|
|
2765
|
+
});
|
|
2766
|
+
return {
|
|
2767
|
+
issueRecords: upsertIssueOrchestration(issueRecords, {
|
|
2768
|
+
issueId: recoveredRecord.issueId,
|
|
2769
|
+
identifier: recoveredRecord.issueIdentifier,
|
|
2770
|
+
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey({
|
|
2771
|
+
projectId: tenant.projectId,
|
|
2772
|
+
adapter: tenant.tracker.adapter,
|
|
2773
|
+
issueSubjectId: recoveredRecord.issueSubjectId
|
|
2774
|
+
}, recoveredRecord.issueIdentifier),
|
|
2775
|
+
state: "running",
|
|
2776
|
+
currentRunId: recoveredRecord.runId,
|
|
2777
|
+
retryEntry: null,
|
|
2778
|
+
updatedAt: now.toISOString()
|
|
2779
|
+
}),
|
|
2780
|
+
recovered: true
|
|
2781
|
+
};
|
|
2782
|
+
}
|
|
2783
|
+
async releaseRetryingRun(run, issueRecords, now) {
|
|
2784
|
+
const suppressedRun = {
|
|
2785
|
+
...run,
|
|
2786
|
+
status: "suppressed",
|
|
2787
|
+
processId: null,
|
|
2788
|
+
completedAt: now.toISOString(),
|
|
2789
|
+
updatedAt: now.toISOString(),
|
|
2790
|
+
nextRetryAt: null,
|
|
2791
|
+
runPhase: "canceled_by_reconciliation",
|
|
2792
|
+
lastError: "Retry canceled because the tracker issue is no longer actionable."
|
|
2793
|
+
};
|
|
2794
|
+
await this.store.saveRun(suppressedRun);
|
|
2795
|
+
this.logVerbose(`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`);
|
|
2796
|
+
return {
|
|
2797
|
+
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
2798
|
+
recovered: false
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
async loadProjectPollInterval(tenant) {
|
|
2802
|
+
const intervals = await Promise.all(tenant.repositories.map(async (repository) => {
|
|
2803
|
+
const resolution = await this.loadProjectWorkflow(tenant, repository);
|
|
2804
|
+
return isUsableWorkflowResolution(resolution) ? resolution.workflow.polling.intervalMs : NaN;
|
|
2805
|
+
}));
|
|
2806
|
+
const validIntervals = intervals.filter((value) => Number.isFinite(value) && value > 0);
|
|
2807
|
+
return validIntervals.length ? Math.min(...validIntervals) : DEFAULT_POLL_INTERVAL_MS;
|
|
2808
|
+
}
|
|
2809
|
+
async loadProjectMaxConcurrentByState(tenant) {
|
|
2810
|
+
const result = {};
|
|
2811
|
+
const resolutions = await Promise.all(tenant.repositories.map(async (repository) => {
|
|
2812
|
+
try {
|
|
2813
|
+
return await this.loadProjectWorkflow(tenant, repository);
|
|
2814
|
+
} catch {
|
|
2815
|
+
return null;
|
|
2816
|
+
}
|
|
2817
|
+
}));
|
|
2818
|
+
for (const resolution of resolutions) {
|
|
2819
|
+
if (!resolution)
|
|
2820
|
+
continue;
|
|
2821
|
+
if (!isUsableWorkflowResolution(resolution))
|
|
2822
|
+
continue;
|
|
2823
|
+
const stateLimits = resolution.workflow.agent.maxConcurrentAgentsByState;
|
|
2824
|
+
for (const [state, limit] of Object.entries(stateLimits)) {
|
|
2825
|
+
const existing = result[state];
|
|
2826
|
+
const numLimit = typeof limit === "number" ? limit : Number(limit);
|
|
2827
|
+
result[state] = existing === void 0 ? numLimit : Math.min(existing, numLimit);
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
return result;
|
|
2831
|
+
}
|
|
2832
|
+
async loadRetryPolicy(tenant, repository) {
|
|
2833
|
+
try {
|
|
2834
|
+
const resolution = await this.loadProjectWorkflow(tenant, repository);
|
|
2835
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
2836
|
+
return null;
|
|
2837
|
+
}
|
|
2838
|
+
return {
|
|
2839
|
+
baseDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.retryBaseDelayMs,
|
|
2840
|
+
maxDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.maxRetryBackoffMs,
|
|
2841
|
+
stallTimeoutMs: resolution.workflow.codex.stallTimeoutMs
|
|
2842
|
+
};
|
|
2843
|
+
} catch {
|
|
2844
|
+
if (!this.dependencies.retryBackoffMs) {
|
|
2845
|
+
return null;
|
|
2846
|
+
}
|
|
2847
|
+
return {
|
|
2848
|
+
baseDelayMs: this.dependencies.retryBackoffMs,
|
|
2849
|
+
maxDelayMs: this.dependencies.retryBackoffMs,
|
|
2850
|
+
stallTimeoutMs: null
|
|
2851
|
+
};
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
async getProjectConcurrency(project) {
|
|
2855
|
+
if (this.dependencies.concurrency !== void 0) {
|
|
2856
|
+
return this.dependencies.concurrency;
|
|
2857
|
+
}
|
|
2858
|
+
const limits = await Promise.all(project.repositories.map(async (repository) => {
|
|
2859
|
+
try {
|
|
2860
|
+
const resolution = await this.loadProjectWorkflow(project, repository);
|
|
2861
|
+
return isUsableWorkflowResolution(resolution) ? resolution.workflow.agent.maxConcurrentAgents : NaN;
|
|
2862
|
+
} catch {
|
|
2863
|
+
return NaN;
|
|
2864
|
+
}
|
|
2865
|
+
}));
|
|
2866
|
+
const validLimits = limits.filter((value) => Number.isFinite(value) && value >= 0);
|
|
2867
|
+
return validLimits.length ? Math.min(...validLimits) : DEFAULT_CONCURRENCY;
|
|
2868
|
+
}
|
|
2869
|
+
async resolveWorkflowResolution(repository, cacheRoot, resolution, changed) {
|
|
2870
|
+
const cacheKey = this.workflowCacheKey(repository);
|
|
2871
|
+
if (resolution.isValid) {
|
|
2872
|
+
const effectiveResolution = {
|
|
2873
|
+
...resolution,
|
|
2874
|
+
isValid: true,
|
|
2875
|
+
usedLastKnownGood: false,
|
|
2876
|
+
validationError: null
|
|
2877
|
+
};
|
|
2878
|
+
let workflowPath = effectiveResolution.workflowPath;
|
|
2879
|
+
try {
|
|
2880
|
+
workflowPath = await this.persistLastKnownGoodWorkflow(cacheRoot, effectiveResolution) ?? effectiveResolution.workflowPath;
|
|
2881
|
+
} catch {
|
|
2882
|
+
workflowPath = effectiveResolution.workflowPath;
|
|
2883
|
+
}
|
|
2884
|
+
this.lastKnownGoodWorkflows.set(cacheKey, {
|
|
2885
|
+
...effectiveResolution,
|
|
2886
|
+
workflowPath
|
|
2887
|
+
});
|
|
2888
|
+
this.lastReportedWorkflowErrors.delete(cacheKey);
|
|
2889
|
+
return effectiveResolution;
|
|
2890
|
+
}
|
|
2891
|
+
const cached = this.lastKnownGoodWorkflows.get(cacheKey);
|
|
2892
|
+
const message = resolution.validationError ?? "Invalid repository WORKFLOW.md";
|
|
2893
|
+
const previousMessage = this.lastReportedWorkflowErrors.get(cacheKey);
|
|
2894
|
+
if (changed || previousMessage !== message) {
|
|
2895
|
+
process.stderr.write(`[orchestrator] failed to reload WORKFLOW.md for ${repository.owner}/${repository.name}: ${message}
|
|
2896
|
+
`);
|
|
2897
|
+
this.lastReportedWorkflowErrors.set(cacheKey, message);
|
|
2898
|
+
}
|
|
2899
|
+
if (!cached) {
|
|
2900
|
+
return resolution;
|
|
2901
|
+
}
|
|
2902
|
+
return {
|
|
2903
|
+
...cached,
|
|
2904
|
+
workflowPath: cached.workflowPath,
|
|
2905
|
+
isValid: false,
|
|
2906
|
+
usedLastKnownGood: true,
|
|
2907
|
+
validationError: message
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
async persistLastKnownGoodWorkflow(cacheRoot, resolution) {
|
|
2911
|
+
if (!resolution.workflowPath) {
|
|
2912
|
+
return null;
|
|
2913
|
+
}
|
|
2914
|
+
const snapshotPath = this.lastKnownGoodWorkflowPath(cacheRoot);
|
|
2915
|
+
const markdown = await readFile3(resolution.workflowPath, "utf8");
|
|
2916
|
+
await mkdir3(join3(cacheRoot, "last-known-good"), { recursive: true });
|
|
2917
|
+
await writeFile3(snapshotPath, markdown, "utf8");
|
|
2918
|
+
return snapshotPath;
|
|
2919
|
+
}
|
|
2920
|
+
lastKnownGoodWorkflowPath(cacheRoot) {
|
|
2921
|
+
return join3(cacheRoot, "last-known-good", "WORKFLOW.md");
|
|
2922
|
+
}
|
|
2923
|
+
workflowCacheKey(repository) {
|
|
2924
|
+
return `${repository.owner}/${repository.name}:${this.normalizeRepositoryCloneUrl(repository.cloneUrl)}`;
|
|
2925
|
+
}
|
|
2926
|
+
normalizeRepositoryCloneUrl(cloneUrl) {
|
|
2927
|
+
if (cloneUrl.startsWith("file://")) {
|
|
2928
|
+
try {
|
|
2929
|
+
return fileURLToPath(cloneUrl);
|
|
2930
|
+
} catch {
|
|
2931
|
+
return cloneUrl;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
return cloneUrl;
|
|
2935
|
+
}
|
|
2936
|
+
isProcessRunning(processId) {
|
|
2937
|
+
if (this.dependencies.isProcessRunning) {
|
|
2938
|
+
return this.dependencies.isProcessRunning(processId);
|
|
2939
|
+
}
|
|
2940
|
+
try {
|
|
2941
|
+
process.kill(-processId, 0);
|
|
2942
|
+
return true;
|
|
2943
|
+
} catch {
|
|
2944
|
+
return false;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
sendSignal(processId, signal) {
|
|
2948
|
+
try {
|
|
2949
|
+
const kill = this.dependencies.killImpl;
|
|
2950
|
+
if (kill) {
|
|
2951
|
+
kill(processId, signal);
|
|
2952
|
+
} else {
|
|
2953
|
+
process.kill(-processId, signal);
|
|
2954
|
+
}
|
|
2955
|
+
} catch {
|
|
2956
|
+
this.retireWorkerPid(processId);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
pruneExitedWorkerPids() {
|
|
2960
|
+
for (const pid of [...this.activeWorkerPids]) {
|
|
2961
|
+
if (!this.isProcessRunning(pid)) {
|
|
2962
|
+
this.retireWorkerPid(pid);
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
retireWorkerPid(processId) {
|
|
2967
|
+
if (processId) {
|
|
2968
|
+
this.activeWorkerPids.delete(processId);
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
/**
|
|
2972
|
+
* Clean up the issue workspace for a terminal issue.
|
|
2973
|
+
*
|
|
2974
|
+
* Runs the `before_remove` hook if configured. Hook failures are logged and
|
|
2975
|
+
* ignored so workspace cleanup still proceeds per spec 9.4. The workspace
|
|
2976
|
+
* directory is removed and the record set to `removed`. Orchestration
|
|
2977
|
+
* records (runs) are preserved.
|
|
2978
|
+
*/
|
|
2979
|
+
async cleanupTerminalIssueWorkspace(tenant, issue, now, workflowResolution) {
|
|
2980
|
+
const issueSubjectId = issue.id;
|
|
2981
|
+
const identity = {
|
|
2982
|
+
projectId: tenant.projectId,
|
|
2983
|
+
adapter: issue.tracker.adapter,
|
|
2984
|
+
issueSubjectId
|
|
2985
|
+
};
|
|
2986
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(identity, issue.identifier);
|
|
2987
|
+
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(identity);
|
|
2988
|
+
const orchestrationRecord = (await this.store.loadProjectIssueOrchestrations(tenant.projectId)).find((record) => record.issueId === issue.id);
|
|
2989
|
+
const workspaceRecord = (orchestrationRecord ? await this.store.loadIssueWorkspace(tenant.projectId, orchestrationRecord.workspaceKey) : null) ?? await this.store.loadIssueWorkspace(tenant.projectId, preferredWorkspaceKey) ?? (legacyWorkspaceKey === preferredWorkspaceKey ? null : await this.store.loadIssueWorkspace(tenant.projectId, legacyWorkspaceKey));
|
|
2990
|
+
if (!workspaceRecord || workspaceRecord.status === "removed") {
|
|
2991
|
+
return;
|
|
2992
|
+
}
|
|
2993
|
+
const pendingRecord = {
|
|
2994
|
+
...workspaceRecord,
|
|
2995
|
+
status: "cleanup_pending",
|
|
2996
|
+
updatedAt: now.toISOString()
|
|
2997
|
+
};
|
|
2998
|
+
await this.store.saveIssueWorkspace(pendingRecord);
|
|
2999
|
+
const hookResult = await this.runHook("before_remove", tenant, workspaceRecord.repositoryPath, issue.repository, {
|
|
3000
|
+
projectId: tenant.projectId,
|
|
3001
|
+
workspaceKey: workspaceRecord.workspaceKey,
|
|
3002
|
+
issueSubjectId,
|
|
3003
|
+
issueIdentifier: issue.identifier,
|
|
3004
|
+
workspacePath: workspaceRecord.workspacePath,
|
|
3005
|
+
repositoryPath: workspaceRecord.repositoryPath
|
|
3006
|
+
}, workflowResolution);
|
|
3007
|
+
if (hookResult && hookResult.outcome !== "success" && hookResult.outcome !== "skipped") {
|
|
3008
|
+
const errorMessage = hookResult.error ?? `before_remove hook ${hookResult.outcome}`;
|
|
3009
|
+
console.warn(`[orchestrator] before_remove hook failed for ${issue.identifier}; continuing cleanup: ${errorMessage}`);
|
|
3010
|
+
}
|
|
3011
|
+
try {
|
|
3012
|
+
await rm3(workspaceRecord.workspacePath, { recursive: true, force: true });
|
|
3013
|
+
} catch {
|
|
3014
|
+
}
|
|
3015
|
+
const removedRecord = {
|
|
3016
|
+
...workspaceRecord,
|
|
3017
|
+
status: "removed",
|
|
3018
|
+
updatedAt: now.toISOString(),
|
|
3019
|
+
lastError: null
|
|
3020
|
+
};
|
|
3021
|
+
await this.store.saveIssueWorkspace(removedRecord);
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
3024
|
+
function hasTokenUsage(tokenUsage) {
|
|
3025
|
+
return Boolean(tokenUsage && (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0 || tokenUsage.totalTokens > 0));
|
|
3026
|
+
}
|
|
3027
|
+
function isRecord(value) {
|
|
3028
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
3029
|
+
}
|
|
3030
|
+
function resolveProjectRateLimits(runs, issues) {
|
|
3031
|
+
let latestRunRateLimits = null;
|
|
3032
|
+
let latestRunTimestamp = -Infinity;
|
|
3033
|
+
for (const run of runs) {
|
|
3034
|
+
if (!isRecord(run.rateLimits)) {
|
|
3035
|
+
continue;
|
|
3036
|
+
}
|
|
3037
|
+
const timestamp = parseTimestampMs(run.lastEventAt ?? run.updatedAt ?? run.startedAt);
|
|
3038
|
+
const sortableTimestamp = timestamp ?? -Infinity;
|
|
3039
|
+
if (sortableTimestamp >= latestRunTimestamp) {
|
|
3040
|
+
latestRunTimestamp = sortableTimestamp;
|
|
3041
|
+
latestRunRateLimits = run.rateLimits;
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
if (latestRunRateLimits) {
|
|
3045
|
+
return latestRunRateLimits;
|
|
3046
|
+
}
|
|
3047
|
+
for (const issue of issues) {
|
|
3048
|
+
if (isRecord(issue.rateLimits)) {
|
|
3049
|
+
return issue.rateLimits;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
return null;
|
|
3053
|
+
}
|
|
3054
|
+
function buildRuntimeSession(existing, sessionId, threadId, status, startedAt, updatedAt, exitClassification = void 0) {
|
|
3055
|
+
if (existing === void 0 && sessionId === null && threadId === null && status === null && (exitClassification === void 0 || exitClassification === null)) {
|
|
3056
|
+
return void 0;
|
|
3057
|
+
}
|
|
3058
|
+
return {
|
|
3059
|
+
sessionId: sessionId ?? existing?.sessionId ?? null,
|
|
3060
|
+
threadId: threadId ?? existing?.threadId ?? null,
|
|
3061
|
+
status: status ?? existing?.status ?? null,
|
|
3062
|
+
startedAt: existing?.startedAt ?? startedAt,
|
|
3063
|
+
updatedAt,
|
|
3064
|
+
exitClassification: exitClassification === void 0 ? existing?.exitClassification ?? null : exitClassification
|
|
3065
|
+
};
|
|
3066
|
+
}
|
|
3067
|
+
function resolvePersistedCumulativeTurnCount(run) {
|
|
3068
|
+
return run.cumulativeTurnCount ?? run.turnCount ?? 0;
|
|
3069
|
+
}
|
|
3070
|
+
function hasConvergenceLockedRun(runs, issueId, issueState) {
|
|
3071
|
+
const latestRun = runs.filter((run) => run.issueId === issueId).sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime())[0];
|
|
3072
|
+
return latestRun?.runtimeSession?.exitClassification === "convergence-detected" && latestRun.issueState === issueState;
|
|
3073
|
+
}
|
|
3074
|
+
function resolveIssueBudgetSnapshot(runs, issueId) {
|
|
3075
|
+
const issueRuns = runs.filter((run) => run.issueId === issueId);
|
|
3076
|
+
const startedAtCandidates = issueRuns.map((run) => run.startedAt).filter((value) => typeof value === "string");
|
|
3077
|
+
return {
|
|
3078
|
+
cumulativeTurnCount: issueRuns.reduce((total, run) => total + resolvePersistedCumulativeTurnCount(run), 0),
|
|
3079
|
+
tokenUsage: issueRuns.reduce((total, run) => ({
|
|
3080
|
+
inputTokens: total.inputTokens + (run.tokenUsage?.inputTokens ?? 0),
|
|
3081
|
+
outputTokens: total.outputTokens + (run.tokenUsage?.outputTokens ?? 0),
|
|
3082
|
+
totalTokens: total.totalTokens + (run.tokenUsage?.totalTokens ?? 0)
|
|
3083
|
+
}), {
|
|
3084
|
+
inputTokens: 0,
|
|
3085
|
+
outputTokens: 0,
|
|
3086
|
+
totalTokens: 0
|
|
3087
|
+
}),
|
|
3088
|
+
sessionStartedAt: startedAtCandidates.sort((left, right) => left.localeCompare(right))[0] ?? null
|
|
3089
|
+
};
|
|
3090
|
+
}
|
|
3091
|
+
function isIssueBudgetExceeded(snapshot, now, env = process.env) {
|
|
3092
|
+
const globalMaxTurns = parsePositiveInteger(env.SYMPHONY_GLOBAL_MAX_TURNS ?? "");
|
|
3093
|
+
if (globalMaxTurns !== null && snapshot.cumulativeTurnCount >= globalMaxTurns) {
|
|
3094
|
+
return true;
|
|
3095
|
+
}
|
|
3096
|
+
const maxTokens = parsePositiveInteger(env.SYMPHONY_MAX_TOKENS ?? "");
|
|
3097
|
+
if (maxTokens !== null && snapshot.tokenUsage.totalTokens >= maxTokens) {
|
|
3098
|
+
return true;
|
|
3099
|
+
}
|
|
3100
|
+
const sessionTimeoutMs = parsePositiveInteger(env.SYMPHONY_SESSION_TIMEOUT_MS ?? "");
|
|
3101
|
+
if (sessionTimeoutMs === null || snapshot.sessionStartedAt === null) {
|
|
3102
|
+
return false;
|
|
3103
|
+
}
|
|
3104
|
+
return now.getTime() - new Date(snapshot.sessionStartedAt).getTime() >= sessionTimeoutMs;
|
|
3105
|
+
}
|
|
3106
|
+
function parsePositiveInteger(value) {
|
|
3107
|
+
const parsed = Number(value);
|
|
3108
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
3109
|
+
return null;
|
|
3110
|
+
}
|
|
3111
|
+
return Math.floor(parsed);
|
|
3112
|
+
}
|
|
3113
|
+
function resolveCumulativeTurnCount(run, turnCount) {
|
|
3114
|
+
const carriedTotal = resolvePersistedCumulativeTurnCount(run);
|
|
3115
|
+
if (turnCount === null) {
|
|
3116
|
+
return carriedTotal;
|
|
3117
|
+
}
|
|
3118
|
+
const previousSessionTurnCount = run.turnCount ?? 0;
|
|
3119
|
+
const baseTurnCount = Math.max(0, carriedTotal - previousSessionTurnCount);
|
|
3120
|
+
return baseTurnCount + turnCount;
|
|
3121
|
+
}
|
|
3122
|
+
function isTerminalTurnEvent(event) {
|
|
3123
|
+
return event === "turn/completed" || event === "turn/failed" || event === "turn/cancelled";
|
|
3124
|
+
}
|
|
3125
|
+
function resolveLastTurnSummaryCandidate(event, lastError) {
|
|
3126
|
+
if (typeof lastError === "string" && lastError.trim()) {
|
|
3127
|
+
return lastError.trim();
|
|
3128
|
+
}
|
|
3129
|
+
return typeof event === "string" && isTerminalTurnEvent(event) ? event : null;
|
|
3130
|
+
}
|
|
3131
|
+
function resolveLastTurnSummary(existing, candidate) {
|
|
3132
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
3133
|
+
return candidate.trim();
|
|
3134
|
+
}
|
|
3135
|
+
return existing ?? null;
|
|
3136
|
+
}
|
|
3137
|
+
function canApplyWorkerChannelUpdate(status) {
|
|
3138
|
+
return status === "running" || status === "retrying";
|
|
3139
|
+
}
|
|
3140
|
+
function resolveChannelSessionId(sessionInfo) {
|
|
3141
|
+
if (!sessionInfo) {
|
|
3142
|
+
return null;
|
|
3143
|
+
}
|
|
3144
|
+
return sessionInfo.sessionId ?? (sessionInfo.threadId && sessionInfo.turnId ? `${sessionInfo.threadId}-${sessionInfo.turnId}` : null);
|
|
3145
|
+
}
|
|
3146
|
+
function resolveWorkerCommand() {
|
|
3147
|
+
if (process.env.SYMPHONY_WORKER_COMMAND) {
|
|
3148
|
+
return process.env.SYMPHONY_WORKER_COMMAND;
|
|
3149
|
+
}
|
|
3150
|
+
try {
|
|
3151
|
+
const workerUrl = import.meta.resolve("@gh-symphony/worker");
|
|
3152
|
+
return `node ${fileURLToPath(workerUrl)}`;
|
|
3153
|
+
} catch {
|
|
3154
|
+
try {
|
|
3155
|
+
const bundledWorker = join3(fileURLToPath(new URL(".", import.meta.url)), "worker-entry.js");
|
|
3156
|
+
return `node ${bundledWorker}`;
|
|
3157
|
+
} catch {
|
|
3158
|
+
return DEFAULT_WORKER_COMMAND;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
function createStore(runtimeRoot = ".runtime", options = {}) {
|
|
3163
|
+
return new OrchestratorFsStore(runtimeRoot, options);
|
|
3164
|
+
}
|
|
3165
|
+
function sortCandidatesForDispatch(candidates) {
|
|
3166
|
+
return [...candidates].sort((a, b) => {
|
|
3167
|
+
if (a.priority !== b.priority) {
|
|
3168
|
+
if (a.priority === null)
|
|
3169
|
+
return 1;
|
|
3170
|
+
if (b.priority === null)
|
|
3171
|
+
return -1;
|
|
3172
|
+
return a.priority - b.priority;
|
|
3173
|
+
}
|
|
3174
|
+
if (a.createdAt !== b.createdAt) {
|
|
3175
|
+
if (a.createdAt === null)
|
|
3176
|
+
return 1;
|
|
3177
|
+
if (b.createdAt === null)
|
|
3178
|
+
return -1;
|
|
3179
|
+
return a.createdAt < b.createdAt ? -1 : 1;
|
|
3180
|
+
}
|
|
3181
|
+
return a.identifier.localeCompare(b.identifier);
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
function createProjectItemsCache() {
|
|
3185
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3186
|
+
return {
|
|
3187
|
+
getOrLoad(key, load) {
|
|
3188
|
+
const cached = entries.get(key);
|
|
3189
|
+
if (cached) {
|
|
3190
|
+
return cached;
|
|
3191
|
+
}
|
|
3192
|
+
const pending = load().catch((error) => {
|
|
3193
|
+
entries.delete(key);
|
|
3194
|
+
throw error;
|
|
3195
|
+
});
|
|
3196
|
+
entries.set(key, pending);
|
|
3197
|
+
return pending;
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
function wait2(ms) {
|
|
3202
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
3203
|
+
}
|
|
3204
|
+
function createRunId(now, projectId, issueIdentifier) {
|
|
3205
|
+
return [
|
|
3206
|
+
projectId,
|
|
3207
|
+
issueIdentifier.replace(/[^a-zA-Z0-9]+/g, "-"),
|
|
3208
|
+
now.getTime().toString(36)
|
|
3209
|
+
].join("-");
|
|
3210
|
+
}
|
|
3211
|
+
function isIssueOrchestrationClaimed(state) {
|
|
3212
|
+
return state === "claimed" || state === "running" || state === "retry_queued";
|
|
3213
|
+
}
|
|
3214
|
+
function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
3215
|
+
const existingRecord = issueRecords.find((record) => record.issueId === nextRecord.issueId) ?? null;
|
|
3216
|
+
const remaining = issueRecords.filter((record) => record.issueId !== nextRecord.issueId);
|
|
3217
|
+
return [
|
|
3218
|
+
...remaining,
|
|
3219
|
+
{
|
|
3220
|
+
...nextRecord,
|
|
3221
|
+
completedOnce: nextRecord.completedOnce ?? existingRecord?.completedOnce ?? false
|
|
3222
|
+
}
|
|
3223
|
+
];
|
|
3224
|
+
}
|
|
3225
|
+
function releaseIssueOrchestration(issueRecords, issueId, now) {
|
|
3226
|
+
return issueRecords.map((record) => record.issueId === issueId ? {
|
|
3227
|
+
...record,
|
|
3228
|
+
state: "released",
|
|
3229
|
+
currentRunId: null,
|
|
3230
|
+
retryEntry: null,
|
|
3231
|
+
updatedAt: now.toISOString()
|
|
3232
|
+
} : record);
|
|
3233
|
+
}
|
|
3234
|
+
function isActiveRunStatus(status) {
|
|
3235
|
+
return status === "pending" || status === "starting" || status === "running" || status === "retrying";
|
|
3236
|
+
}
|
|
3237
|
+
|
|
3238
|
+
// ../orchestrator/dist/lock.js
|
|
3239
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3240
|
+
import { mkdir as mkdir4, open as open2, readFile as readFile4, rm as rm4 } from "fs/promises";
|
|
3241
|
+
import { dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve2 } from "path";
|
|
3242
|
+
import { setTimeout as delay } from "timers/promises";
|
|
3243
|
+
var LOCK_READ_RETRY_DELAY_MS = 10;
|
|
3244
|
+
var LOCK_READ_RETRY_LIMIT = 20;
|
|
3245
|
+
async function acquireProjectLock(input) {
|
|
3246
|
+
assertValidProjectId(input.projectId);
|
|
3247
|
+
const pid = input.pid ?? process.pid;
|
|
3248
|
+
const startedAt = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
3249
|
+
const ownerToken = `${pid}:${randomUUID2()}`;
|
|
3250
|
+
const lockPath = resolveProjectLockPath(input.runtimeRoot, input.projectId);
|
|
3251
|
+
const record = { ownerToken, pid, startedAt };
|
|
3252
|
+
let invalidReadAttempts = 0;
|
|
3253
|
+
for (; ; ) {
|
|
3254
|
+
try {
|
|
3255
|
+
await mkdir4(dirname2(lockPath), { recursive: true });
|
|
3256
|
+
const handle = await open2(lockPath, "wx");
|
|
3257
|
+
try {
|
|
3258
|
+
await handle.writeFile(JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
3259
|
+
} finally {
|
|
3260
|
+
await handle.close();
|
|
3261
|
+
}
|
|
3262
|
+
return { lockPath, ownerToken, pid, startedAt };
|
|
3263
|
+
} catch (error) {
|
|
3264
|
+
if (!isAlreadyExistsError2(error)) {
|
|
3265
|
+
throw error;
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
const existing = await readProjectLock(lockPath);
|
|
3269
|
+
if (existing.status === "missing") {
|
|
3270
|
+
invalidReadAttempts = 0;
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
if (existing.status === "invalid") {
|
|
3274
|
+
invalidReadAttempts += 1;
|
|
3275
|
+
if (invalidReadAttempts >= LOCK_READ_RETRY_LIMIT) {
|
|
3276
|
+
throw new Error(`Project "${input.projectId}" lock file is unreadable at "${lockPath}".`);
|
|
3277
|
+
}
|
|
3278
|
+
await delay(LOCK_READ_RETRY_DELAY_MS);
|
|
3279
|
+
continue;
|
|
3280
|
+
}
|
|
3281
|
+
invalidReadAttempts = 0;
|
|
3282
|
+
if ((input.isProcessRunning ?? isProcessRunning)(existing.record.pid)) {
|
|
3283
|
+
throw new Error(`Project "${input.projectId}" is already running (PID ${existing.record.pid}).`);
|
|
3284
|
+
}
|
|
3285
|
+
await rm4(lockPath, { force: true });
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
async function releaseProjectLock(lock) {
|
|
3289
|
+
if (!lock) {
|
|
3290
|
+
return;
|
|
3291
|
+
}
|
|
3292
|
+
try {
|
|
3293
|
+
const existing = await readProjectLock(lock.lockPath);
|
|
3294
|
+
if (existing.status !== "valid" || existing.record.ownerToken !== lock.ownerToken) {
|
|
3295
|
+
return;
|
|
3296
|
+
}
|
|
3297
|
+
} catch (error) {
|
|
3298
|
+
if (isMissingFileError2(error)) {
|
|
3299
|
+
return;
|
|
3300
|
+
}
|
|
3301
|
+
throw error;
|
|
3302
|
+
}
|
|
3303
|
+
await rm4(lock.lockPath, { force: true });
|
|
3304
|
+
}
|
|
3305
|
+
async function readProjectLock(lockPath) {
|
|
3306
|
+
try {
|
|
3307
|
+
const raw = await readFile4(lockPath, "utf8");
|
|
3308
|
+
const record = parseProjectLock(raw);
|
|
3309
|
+
if (!record) {
|
|
3310
|
+
return { status: "invalid" };
|
|
3311
|
+
}
|
|
3312
|
+
return { status: "valid", record };
|
|
3313
|
+
} catch (error) {
|
|
3314
|
+
if (isMissingFileError2(error)) {
|
|
3315
|
+
return { status: "missing" };
|
|
3316
|
+
}
|
|
3317
|
+
throw error;
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
function assertValidProjectId(projectId) {
|
|
3321
|
+
if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
|
|
3322
|
+
throw new Error(`Invalid project ID "${projectId}". Project IDs must not contain path separators or traversal segments.`);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
function resolveProjectLockPath(runtimeRoot, projectId) {
|
|
3326
|
+
const store = new OrchestratorFsStore(runtimeRoot);
|
|
3327
|
+
const projectsRoot = resolve2(runtimeRoot, "projects");
|
|
3328
|
+
const projectDir = resolve2(store.projectDir(projectId));
|
|
3329
|
+
const relativeProjectDir = relative2(projectsRoot, projectDir);
|
|
3330
|
+
if (relativeProjectDir.length === 0 || relativeProjectDir.startsWith("..") || isAbsolute(relativeProjectDir)) {
|
|
3331
|
+
throw new Error(`Invalid project ID "${projectId}". Project lock path must stay within "${projectsRoot}".`);
|
|
3332
|
+
}
|
|
3333
|
+
return join4(projectDir, ".lock");
|
|
3334
|
+
}
|
|
3335
|
+
function parseProjectLock(raw) {
|
|
3336
|
+
try {
|
|
3337
|
+
const parsed = JSON.parse(raw);
|
|
3338
|
+
if (typeof parsed.ownerToken !== "string" || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.startedAt !== "string") {
|
|
3339
|
+
return null;
|
|
3340
|
+
}
|
|
3341
|
+
return {
|
|
3342
|
+
ownerToken: parsed.ownerToken,
|
|
3343
|
+
pid: parsed.pid,
|
|
3344
|
+
startedAt: parsed.startedAt
|
|
3345
|
+
};
|
|
3346
|
+
} catch {
|
|
3347
|
+
return null;
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
function isProcessRunning(pid) {
|
|
3351
|
+
try {
|
|
3352
|
+
process.kill(pid, 0);
|
|
3353
|
+
return true;
|
|
3354
|
+
} catch (error) {
|
|
3355
|
+
return !(error && typeof error === "object" && "code" in error && error.code === "ESRCH");
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
function isAlreadyExistsError2(error) {
|
|
3359
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
|
3360
|
+
}
|
|
3361
|
+
function isMissingFileError2(error) {
|
|
3362
|
+
return Boolean(error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"));
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// ../orchestrator/dist/index.js
|
|
3366
|
+
import { pathToFileURL } from "url";
|
|
3367
|
+
import { resolve as resolve3 } from "path";
|
|
3368
|
+
function resolveOrchestratorLogLevel(value) {
|
|
3369
|
+
if (!value || value === "normal") {
|
|
3370
|
+
return "normal";
|
|
3371
|
+
}
|
|
3372
|
+
if (value === "verbose") {
|
|
3373
|
+
return "verbose";
|
|
3374
|
+
}
|
|
3375
|
+
throw new Error(`Unsupported log level: ${value}. Supported values: normal, verbose.`);
|
|
3376
|
+
}
|
|
3377
|
+
async function runCli(argv, dependencies = {}) {
|
|
3378
|
+
const [command = "run-once", ...args] = argv;
|
|
3379
|
+
const parsed = parseArgs(args);
|
|
3380
|
+
if (parsed.projectId) {
|
|
3381
|
+
assertValidProjectId(parsed.projectId);
|
|
3382
|
+
}
|
|
3383
|
+
const runtimeRoot = resolve3(parsed.runtimeRoot ?? ".runtime");
|
|
3384
|
+
const stderr = dependencies.stderr ?? process.stderr;
|
|
3385
|
+
const eventsDir = resolveOptionalPath(parsed.eventsDir ?? process.env.SYMPHONY_EVENTS_DIR);
|
|
3386
|
+
const logLevel = resolveOrchestratorLogLevel(parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL);
|
|
3387
|
+
const service = await dependencies.createService?.(runtimeRoot, parsed.projectId, {
|
|
3388
|
+
eventsDir,
|
|
3389
|
+
logLevel,
|
|
3390
|
+
stderr
|
|
3391
|
+
}) ?? await createServiceForRuntime(runtimeRoot, parsed.projectId, {
|
|
3392
|
+
eventsDir,
|
|
3393
|
+
logLevel,
|
|
3394
|
+
stderr
|
|
3395
|
+
});
|
|
3396
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
3397
|
+
const exitProcess = dependencies.exitProcess ?? process.exit;
|
|
3398
|
+
const signalTarget = dependencies.signalTarget ?? process;
|
|
3399
|
+
switch (command) {
|
|
3400
|
+
case "run": {
|
|
3401
|
+
let lock = null;
|
|
3402
|
+
let cleanupPromise = null;
|
|
3403
|
+
let shuttingDownForSignal = false;
|
|
3404
|
+
const cleanup = async () => {
|
|
3405
|
+
if (cleanupPromise) {
|
|
3406
|
+
return cleanupPromise;
|
|
3407
|
+
}
|
|
3408
|
+
cleanupPromise = (async () => {
|
|
3409
|
+
let cleanupError;
|
|
3410
|
+
const shutdownPromise = service.shutdown();
|
|
3411
|
+
try {
|
|
3412
|
+
await shutdownPromise;
|
|
3413
|
+
} catch (error) {
|
|
3414
|
+
cleanupError = error;
|
|
3415
|
+
} finally {
|
|
3416
|
+
try {
|
|
3417
|
+
await (dependencies.releaseLock ?? releaseProjectLock)(lock);
|
|
3418
|
+
lock = null;
|
|
3419
|
+
} catch (lockError) {
|
|
3420
|
+
cleanupError ??= lockError;
|
|
3421
|
+
}
|
|
3422
|
+
}
|
|
3423
|
+
if (cleanupError) {
|
|
3424
|
+
throw cleanupError;
|
|
3425
|
+
}
|
|
3426
|
+
})();
|
|
3427
|
+
return cleanupPromise;
|
|
3428
|
+
};
|
|
3429
|
+
const handleSignal = (signal) => {
|
|
3430
|
+
shuttingDownForSignal = true;
|
|
3431
|
+
let exitCode = 0;
|
|
3432
|
+
void cleanup().catch((error) => {
|
|
3433
|
+
exitCode = 1;
|
|
3434
|
+
stderr.write(`Failed to shut down orchestrator after ${signal}: ${error instanceof Error ? error.message : String(error)}
|
|
3435
|
+
`);
|
|
3436
|
+
}).finally(() => {
|
|
3437
|
+
exitProcess(exitCode);
|
|
3438
|
+
});
|
|
3439
|
+
};
|
|
3440
|
+
const sigintHandler = () => handleSignal("SIGINT");
|
|
3441
|
+
const sigtermHandler = () => handleSignal("SIGTERM");
|
|
3442
|
+
try {
|
|
3443
|
+
if (parsed.projectId) {
|
|
3444
|
+
lock = await (dependencies.acquireLock ?? acquireProjectLock)({
|
|
3445
|
+
runtimeRoot,
|
|
3446
|
+
projectId: parsed.projectId
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
signalTarget.once("SIGINT", sigintHandler);
|
|
3450
|
+
signalTarget.once("SIGTERM", sigtermHandler);
|
|
3451
|
+
await service.run({
|
|
3452
|
+
issueIdentifier: parsed.issueIdentifier
|
|
3453
|
+
});
|
|
3454
|
+
await cleanup();
|
|
3455
|
+
} finally {
|
|
3456
|
+
signalTarget.off("SIGINT", sigintHandler);
|
|
3457
|
+
signalTarget.off("SIGTERM", sigtermHandler);
|
|
3458
|
+
if (!shuttingDownForSignal) {
|
|
3459
|
+
await cleanup();
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
return;
|
|
3463
|
+
}
|
|
3464
|
+
case "run-once":
|
|
3465
|
+
case "dispatch": {
|
|
3466
|
+
const result = await service.runOnce({
|
|
3467
|
+
issueIdentifier: parsed.issueIdentifier
|
|
3468
|
+
});
|
|
3469
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
case "run-issue": {
|
|
3473
|
+
if (!parsed.projectId || !parsed.issueIdentifier) {
|
|
3474
|
+
throw new Error("run-issue requires --project-id and --issue.");
|
|
3475
|
+
}
|
|
3476
|
+
const result = await service.runOnce({
|
|
3477
|
+
issueIdentifier: parsed.issueIdentifier
|
|
3478
|
+
});
|
|
3479
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
3480
|
+
return;
|
|
3481
|
+
}
|
|
3482
|
+
case "recover": {
|
|
3483
|
+
const result = await service.recover();
|
|
3484
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3487
|
+
case "status": {
|
|
3488
|
+
const result = await service.status();
|
|
3489
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
default:
|
|
3493
|
+
throw new Error(`Unsupported command: ${command}`);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
async function createServiceForRuntime(runtimeRoot, projectId, options) {
|
|
3497
|
+
if (!projectId) {
|
|
3498
|
+
throw new Error("Orchestrator CLI requires --project-id.");
|
|
3499
|
+
}
|
|
3500
|
+
const store = createStore(runtimeRoot, {
|
|
3501
|
+
eventsMirrorRoot: options?.eventsDir
|
|
3502
|
+
});
|
|
3503
|
+
const projectConfig = await store.loadProjectConfig(projectId);
|
|
3504
|
+
if (!projectConfig) {
|
|
3505
|
+
throw new Error(`Project config not found for "${projectId}".`);
|
|
3506
|
+
}
|
|
3507
|
+
return new OrchestratorService(store, projectConfig, options);
|
|
3508
|
+
}
|
|
3509
|
+
async function main() {
|
|
3510
|
+
await runCli(process.argv.slice(2));
|
|
3511
|
+
}
|
|
3512
|
+
function parseArgs(args) {
|
|
3513
|
+
const parsed = {};
|
|
3514
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
3515
|
+
const argument = args[index];
|
|
3516
|
+
const value = args[index + 1];
|
|
3517
|
+
if (!argument?.startsWith("--")) {
|
|
3518
|
+
continue;
|
|
3519
|
+
}
|
|
3520
|
+
switch (argument) {
|
|
3521
|
+
case "--runtime-root":
|
|
3522
|
+
parsed.runtimeRoot = value;
|
|
3523
|
+
index += 1;
|
|
3524
|
+
break;
|
|
3525
|
+
case "--project":
|
|
3526
|
+
case "--project-id":
|
|
3527
|
+
parsed.projectId = value;
|
|
3528
|
+
index += 1;
|
|
3529
|
+
break;
|
|
3530
|
+
case "--issue":
|
|
3531
|
+
parsed.issueIdentifier = value;
|
|
3532
|
+
index += 1;
|
|
3533
|
+
break;
|
|
3534
|
+
case "--events-dir":
|
|
3535
|
+
if (!value || value.startsWith("-")) {
|
|
3536
|
+
throw new Error(`Option '${argument}' argument missing`);
|
|
3537
|
+
}
|
|
3538
|
+
parsed.eventsDir = value;
|
|
3539
|
+
index += 1;
|
|
3540
|
+
break;
|
|
3541
|
+
case "--log-level":
|
|
3542
|
+
if (!value || value.startsWith("-")) {
|
|
3543
|
+
throw new Error(`Option '${argument}' argument missing`);
|
|
3544
|
+
}
|
|
3545
|
+
parsed.logLevel = value;
|
|
3546
|
+
index += 1;
|
|
3547
|
+
break;
|
|
3548
|
+
default:
|
|
3549
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
return parsed;
|
|
3553
|
+
}
|
|
3554
|
+
function resolveOptionalPath(value) {
|
|
3555
|
+
if (!value || value.trim().length === 0) {
|
|
3556
|
+
return void 0;
|
|
3557
|
+
}
|
|
3558
|
+
return resolve3(value.trim());
|
|
3559
|
+
}
|
|
3560
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
3561
|
+
main().catch((error) => {
|
|
3562
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Unknown error"}
|
|
3563
|
+
`);
|
|
3564
|
+
process.exitCode = 1;
|
|
3565
|
+
});
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
export {
|
|
3569
|
+
OrchestratorService,
|
|
3570
|
+
createStore,
|
|
3571
|
+
acquireProjectLock,
|
|
3572
|
+
releaseProjectLock,
|
|
3573
|
+
resolveOrchestratorLogLevel,
|
|
3574
|
+
runCli
|
|
3575
|
+
};
|