@gh-symphony/cli 0.0.13 → 0.0.15
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-IWR4UQEJ.js +2250 -0
- package/dist/chunk-JO3AXHQI.js +130 -0
- package/dist/chunk-M7OSMUTN.js +874 -0
- package/dist/chunk-MVRF7BES.js +68 -0
- package/dist/chunk-RNWX7DQU.js +4617 -0
- package/dist/chunk-ROGRTUFI.js +108 -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-3ELXQ35D.js +678 -0
- package/dist/recover-T6ME6C56.js +130 -0
- package/dist/repo-R3XBIVAX.js +121 -0
- package/dist/run-DYINRZHK.js +107 -0
- package/dist/start-PIFQMIC2.js +15 -0
- package/dist/status-3WK5BWRZ.js +11 -0
- package/dist/stop-AA3AP5M6.js +9 -0
- package/dist/version-VBB62JWI.js +30 -0
- package/package.json +11 -6
- 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,4617 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ../orchestrator/dist/service.js
|
|
4
|
+
import { mkdir as mkdir3, readFile as readFile5, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
5
|
+
import { createWriteStream, mkdirSync } from "fs";
|
|
6
|
+
import { spawn as spawn3 } from "child_process";
|
|
7
|
+
import { join as join4 } from "path";
|
|
8
|
+
import { StringDecoder } from "string_decoder";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
|
|
11
|
+
// ../core/dist/contracts/status-surface.js
|
|
12
|
+
var WORKFLOW_EXECUTION_PHASES = [
|
|
13
|
+
"planning",
|
|
14
|
+
"human-review",
|
|
15
|
+
"implementation",
|
|
16
|
+
"awaiting-merge",
|
|
17
|
+
"completed"
|
|
18
|
+
];
|
|
19
|
+
function isWorkflowExecutionPhase(value) {
|
|
20
|
+
return typeof value === "string" && WORKFLOW_EXECUTION_PHASES.includes(value);
|
|
21
|
+
}
|
|
22
|
+
var SESSION_EXIT_CLASSIFICATIONS = [
|
|
23
|
+
"completed",
|
|
24
|
+
"budget-exceeded",
|
|
25
|
+
"convergence-detected",
|
|
26
|
+
"max-turns-reached",
|
|
27
|
+
"user-input-required",
|
|
28
|
+
"timeout",
|
|
29
|
+
"error"
|
|
30
|
+
];
|
|
31
|
+
function isSessionExitClassification(value) {
|
|
32
|
+
return typeof value === "string" && SESSION_EXIT_CLASSIFICATIONS.includes(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ../core/dist/contracts/run-attempt-phase.js
|
|
36
|
+
var RUN_ATTEMPT_PHASES = [
|
|
37
|
+
"preparing_workspace",
|
|
38
|
+
"building_prompt",
|
|
39
|
+
"launching_agent",
|
|
40
|
+
"initializing_session",
|
|
41
|
+
"streaming_turn",
|
|
42
|
+
"finishing",
|
|
43
|
+
"succeeded",
|
|
44
|
+
"failed",
|
|
45
|
+
"timed_out",
|
|
46
|
+
"stalled",
|
|
47
|
+
"canceled_by_reconciliation"
|
|
48
|
+
];
|
|
49
|
+
function isRunAttemptPhase(value) {
|
|
50
|
+
return typeof value === "string" && RUN_ATTEMPT_PHASES.includes(value);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ../core/dist/contracts/orchestrator-channel.js
|
|
54
|
+
function isRecord(value) {
|
|
55
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
56
|
+
}
|
|
57
|
+
function isTokenUsage(value) {
|
|
58
|
+
if (!isRecord(value)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return typeof value.inputTokens === "number" && typeof value.outputTokens === "number" && typeof value.totalTokens === "number";
|
|
62
|
+
}
|
|
63
|
+
function isSessionInfo(value) {
|
|
64
|
+
if (!isRecord(value)) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return (typeof value.threadId === "string" || value.threadId === null) && (typeof value.turnId === "string" || value.turnId === null) && typeof value.turnCount === "number" && (typeof value.sessionId === "string" || value.sessionId === null) && (!("exitClassification" in value) || value.exitClassification === void 0 || value.exitClassification === null || isSessionExitClassification(value.exitClassification));
|
|
68
|
+
}
|
|
69
|
+
function isNullableString(value) {
|
|
70
|
+
return typeof value === "string" || value === null;
|
|
71
|
+
}
|
|
72
|
+
function isTurnEventBase(value) {
|
|
73
|
+
return typeof value.startedAt === "string" && isNullableString(value.threadId) && isNullableString(value.turnId) && typeof value.turnCount === "number" && isNullableString(value.sessionId);
|
|
74
|
+
}
|
|
75
|
+
function isOrchestratorChannelEvent(value) {
|
|
76
|
+
if (!isRecord(value)) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
if (typeof value.issueId !== "string") {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
if (value.type === "codex_update") {
|
|
83
|
+
if (typeof value.lastEventAt !== "string") {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if ("event" in value && value.event !== void 0 && typeof value.event !== "string") {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if ("tokenUsage" in value && value.tokenUsage !== void 0 && !isTokenUsage(value.tokenUsage)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
if ("rateLimits" in value && value.rateLimits !== void 0 && !isRecord(value.rateLimits)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if ("sessionInfo" in value && value.sessionInfo !== void 0 && !isSessionInfo(value.sessionInfo)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if ("executionPhase" in value && value.executionPhase !== void 0 && value.executionPhase !== null && !isWorkflowExecutionPhase(value.executionPhase)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
if ("runPhase" in value && value.runPhase !== void 0 && value.runPhase !== null && !isRunAttemptPhase(value.runPhase)) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
if ("lastError" in value && value.lastError !== void 0 && value.lastError !== null && typeof value.lastError !== "string") {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
if (value.type === "heartbeat") {
|
|
110
|
+
if (value.lastEventAt !== null && typeof value.lastEventAt !== "string") {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
if (!isTokenUsage(value.tokenUsage)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (value.rateLimits !== null && !isRecord(value.rateLimits)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (value.sessionInfo !== null && !isSessionInfo(value.sessionInfo)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
if (value.executionPhase !== null && !isWorkflowExecutionPhase(value.executionPhase)) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
if (value.runPhase !== null && !isRunAttemptPhase(value.runPhase)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (value.lastError !== null && typeof value.lastError !== "string") {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (value.type === "turn_started") {
|
|
134
|
+
return isTurnEventBase(value);
|
|
135
|
+
}
|
|
136
|
+
if (value.type === "turn_completed") {
|
|
137
|
+
return isTurnEventBase(value) && typeof value.completedAt === "string" && typeof value.durationMs === "number" && isTokenUsage(value.tokenUsage);
|
|
138
|
+
}
|
|
139
|
+
if (value.type === "turn_failed") {
|
|
140
|
+
return isTurnEventBase(value) && typeof value.failedAt === "string" && typeof value.durationMs === "number" && isTokenUsage(value.tokenUsage) && isNullableString(value.error);
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ../core/dist/workflow/lifecycle.js
|
|
146
|
+
var DEFAULT_WORKFLOW_LIFECYCLE = {
|
|
147
|
+
stateFieldName: "Status",
|
|
148
|
+
activeStates: ["Todo", "In Progress"],
|
|
149
|
+
terminalStates: ["Done"],
|
|
150
|
+
blockerCheckStates: ["Todo"]
|
|
151
|
+
};
|
|
152
|
+
function isStateActive(state, lifecycle) {
|
|
153
|
+
return matchesWorkflowState(state, lifecycle.activeStates);
|
|
154
|
+
}
|
|
155
|
+
function isStateTerminal(state, lifecycle) {
|
|
156
|
+
return matchesWorkflowState(state, lifecycle.terminalStates);
|
|
157
|
+
}
|
|
158
|
+
function matchesWorkflowState(state, candidates) {
|
|
159
|
+
const normalizedState = normalizeWorkflowState(state);
|
|
160
|
+
return candidates.some((candidate) => normalizeWorkflowState(candidate) === normalizedState);
|
|
161
|
+
}
|
|
162
|
+
function normalizeWorkflowState(state) {
|
|
163
|
+
return state.trim().toLowerCase();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ../core/dist/workflow/config.js
|
|
167
|
+
var DEFAULT_CODEX_COMMAND = "codex app-server";
|
|
168
|
+
var DEFAULT_AGENT_COMMAND = DEFAULT_CODEX_COMMAND;
|
|
169
|
+
var DEFAULT_HOOK_TIMEOUT_MS = 6e4;
|
|
170
|
+
var DEFAULT_POLL_INTERVAL_MS = 3e4;
|
|
171
|
+
var DEFAULT_MAX_RETRY_BACKOFF_MS = 3e5;
|
|
172
|
+
var DEFAULT_MAX_DELAY_MS = DEFAULT_MAX_RETRY_BACKOFF_MS;
|
|
173
|
+
var DEFAULT_BASE_DELAY_MS = 1e4;
|
|
174
|
+
var DEFAULT_MAX_TURNS = 20;
|
|
175
|
+
var DEFAULT_READ_TIMEOUT_MS = 5e3;
|
|
176
|
+
var DEFAULT_TURN_TIMEOUT_MS = 36e5;
|
|
177
|
+
var DEFAULT_STALL_TIMEOUT_MS = 3e5;
|
|
178
|
+
var DEFAULT_MAX_CONCURRENT_AGENTS = 10;
|
|
179
|
+
var DEFAULT_WORKFLOW_HOOKS = {
|
|
180
|
+
afterCreate: null,
|
|
181
|
+
beforeRun: null,
|
|
182
|
+
afterRun: null,
|
|
183
|
+
beforeRemove: null,
|
|
184
|
+
timeoutMs: DEFAULT_HOOK_TIMEOUT_MS
|
|
185
|
+
};
|
|
186
|
+
var DEFAULT_WORKFLOW_TRACKER = {
|
|
187
|
+
kind: null,
|
|
188
|
+
endpoint: null,
|
|
189
|
+
apiKey: null,
|
|
190
|
+
projectSlug: null,
|
|
191
|
+
activeStates: DEFAULT_WORKFLOW_LIFECYCLE.activeStates,
|
|
192
|
+
terminalStates: DEFAULT_WORKFLOW_LIFECYCLE.terminalStates,
|
|
193
|
+
projectId: null,
|
|
194
|
+
stateFieldName: DEFAULT_WORKFLOW_LIFECYCLE.stateFieldName,
|
|
195
|
+
priorityFieldName: null,
|
|
196
|
+
blockerCheckStates: DEFAULT_WORKFLOW_LIFECYCLE.blockerCheckStates
|
|
197
|
+
};
|
|
198
|
+
var DEFAULT_WORKFLOW_WORKSPACE = {
|
|
199
|
+
root: null
|
|
200
|
+
};
|
|
201
|
+
var DEFAULT_WORKFLOW_AGENT = {
|
|
202
|
+
maxConcurrentAgents: DEFAULT_MAX_CONCURRENT_AGENTS,
|
|
203
|
+
maxRetryBackoffMs: DEFAULT_MAX_RETRY_BACKOFF_MS,
|
|
204
|
+
maxConcurrentAgentsByState: {},
|
|
205
|
+
maxTurns: DEFAULT_MAX_TURNS,
|
|
206
|
+
retryBaseDelayMs: DEFAULT_BASE_DELAY_MS
|
|
207
|
+
};
|
|
208
|
+
var DEFAULT_WORKFLOW_CODEX = {
|
|
209
|
+
command: DEFAULT_CODEX_COMMAND,
|
|
210
|
+
approvalPolicy: null,
|
|
211
|
+
threadSandbox: null,
|
|
212
|
+
turnSandboxPolicy: null,
|
|
213
|
+
turnTimeoutMs: DEFAULT_TURN_TIMEOUT_MS,
|
|
214
|
+
readTimeoutMs: DEFAULT_READ_TIMEOUT_MS,
|
|
215
|
+
stallTimeoutMs: DEFAULT_STALL_TIMEOUT_MS
|
|
216
|
+
};
|
|
217
|
+
var DEFAULT_WORKFLOW_DEFINITION = {
|
|
218
|
+
promptTemplate: "",
|
|
219
|
+
continuationGuidance: null,
|
|
220
|
+
tracker: DEFAULT_WORKFLOW_TRACKER,
|
|
221
|
+
polling: {
|
|
222
|
+
intervalMs: DEFAULT_POLL_INTERVAL_MS
|
|
223
|
+
},
|
|
224
|
+
workspace: DEFAULT_WORKFLOW_WORKSPACE,
|
|
225
|
+
hooks: DEFAULT_WORKFLOW_HOOKS,
|
|
226
|
+
agent: DEFAULT_WORKFLOW_AGENT,
|
|
227
|
+
codex: DEFAULT_WORKFLOW_CODEX,
|
|
228
|
+
lifecycle: DEFAULT_WORKFLOW_LIFECYCLE,
|
|
229
|
+
format: "default",
|
|
230
|
+
githubProjectId: null,
|
|
231
|
+
agentCommand: DEFAULT_CODEX_COMMAND,
|
|
232
|
+
hookPath: null,
|
|
233
|
+
maxConcurrentByState: {}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// ../core/dist/workflow/parser.js
|
|
237
|
+
function parseWorkflowMarkdown(markdown, env = process.env, options = {}) {
|
|
238
|
+
const compatibilityMode = options.compatibilityMode ?? "strict";
|
|
239
|
+
const frontMatterMatch = markdown.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
240
|
+
if (!frontMatterMatch) {
|
|
241
|
+
if (compatibilityMode === "legacy") {
|
|
242
|
+
return parseLegacyWorkflowMarkdown(markdown);
|
|
243
|
+
}
|
|
244
|
+
throw new Error("WORKFLOW.md must use YAML front matter.");
|
|
245
|
+
}
|
|
246
|
+
const [, rawFrontMatter, rawPromptTemplate = ""] = frontMatterMatch;
|
|
247
|
+
const frontMatter = parseFrontMatter(rawFrontMatter);
|
|
248
|
+
const promptTemplate = rawPromptTemplate.trim();
|
|
249
|
+
const tracker = readRequiredObject(frontMatter, "tracker");
|
|
250
|
+
const polling = readObject(frontMatter, "polling");
|
|
251
|
+
const workspace = readObject(frontMatter, "workspace");
|
|
252
|
+
const hooks = readObject(frontMatter, "hooks");
|
|
253
|
+
const agent = readObject(frontMatter, "agent");
|
|
254
|
+
const codex = readRequiredObject(frontMatter, "codex");
|
|
255
|
+
const trackerKind = readRequiredString(tracker, "kind", env);
|
|
256
|
+
const activeStates = readStringList(tracker, "active_states") ?? DEFAULT_WORKFLOW_TRACKER.activeStates;
|
|
257
|
+
const terminalStates = readStringList(tracker, "terminal_states") ?? DEFAULT_WORKFLOW_TRACKER.terminalStates;
|
|
258
|
+
const blockerCheckStates = readStringList(tracker, "blocker_check_states") ?? DEFAULT_WORKFLOW_TRACKER.blockerCheckStates;
|
|
259
|
+
const maxConcurrentAgentsByState = readNumberMap(agent, "max_concurrent_agents_by_state");
|
|
260
|
+
const command = readOptionalString(codex, "command", env) ?? DEFAULT_AGENT_COMMAND;
|
|
261
|
+
const parsed = {
|
|
262
|
+
promptTemplate,
|
|
263
|
+
continuationGuidance: readOptionalWorkflowString(frontMatter, "continuationGuidance", "continuation_guidance", env),
|
|
264
|
+
tracker: {
|
|
265
|
+
kind: trackerKind,
|
|
266
|
+
endpoint: readOptionalString(tracker, "endpoint", env),
|
|
267
|
+
apiKey: readOptionalString(tracker, "api_key", env),
|
|
268
|
+
projectSlug: readOptionalString(tracker, "project_slug", env),
|
|
269
|
+
activeStates,
|
|
270
|
+
terminalStates,
|
|
271
|
+
projectId: readOptionalString(tracker, "project_id", env),
|
|
272
|
+
stateFieldName: readOptionalString(tracker, "state_field", env) ?? DEFAULT_WORKFLOW_TRACKER.stateFieldName,
|
|
273
|
+
priorityFieldName: readOptionalString(tracker, "priority_field", env),
|
|
274
|
+
blockerCheckStates
|
|
275
|
+
},
|
|
276
|
+
polling: {
|
|
277
|
+
intervalMs: readOptionalIntegerLike(polling, "interval_ms") ?? DEFAULT_POLL_INTERVAL_MS
|
|
278
|
+
},
|
|
279
|
+
workspace: {
|
|
280
|
+
root: readOptionalString(workspace, "root", env)
|
|
281
|
+
},
|
|
282
|
+
hooks: {
|
|
283
|
+
afterCreate: readOptionalString(hooks, "after_create", env),
|
|
284
|
+
beforeRun: readOptionalString(hooks, "before_run", env),
|
|
285
|
+
afterRun: readOptionalString(hooks, "after_run", env),
|
|
286
|
+
beforeRemove: readOptionalString(hooks, "before_remove", env),
|
|
287
|
+
timeoutMs: readOptionalIntegerLike(hooks, "timeout_ms") ?? DEFAULT_HOOK_TIMEOUT_MS
|
|
288
|
+
},
|
|
289
|
+
agent: {
|
|
290
|
+
maxConcurrentAgents: readOptionalIntegerLike(agent, "max_concurrent_agents") ?? DEFAULT_MAX_CONCURRENT_AGENTS,
|
|
291
|
+
maxRetryBackoffMs: readOptionalIntegerLike(agent, "max_retry_backoff_ms") ?? DEFAULT_MAX_RETRY_BACKOFF_MS,
|
|
292
|
+
maxConcurrentAgentsByState,
|
|
293
|
+
maxTurns: readOptionalIntegerLike(agent, "max_turns") ?? DEFAULT_MAX_TURNS,
|
|
294
|
+
retryBaseDelayMs: readOptionalIntegerLike(agent, "retry_base_delay_ms") ?? DEFAULT_BASE_DELAY_MS
|
|
295
|
+
},
|
|
296
|
+
codex: {
|
|
297
|
+
command,
|
|
298
|
+
approvalPolicy: readOptionalString(codex, "approval_policy", env),
|
|
299
|
+
threadSandbox: readOptionalString(codex, "thread_sandbox", env),
|
|
300
|
+
turnSandboxPolicy: readOptionalString(codex, "turn_sandbox_policy", env),
|
|
301
|
+
turnTimeoutMs: readOptionalIntegerLike(codex, "turn_timeout_ms") ?? DEFAULT_TURN_TIMEOUT_MS,
|
|
302
|
+
readTimeoutMs: readOptionalIntegerLike(codex, "read_timeout_ms") ?? DEFAULT_READ_TIMEOUT_MS,
|
|
303
|
+
stallTimeoutMs: readOptionalIntegerLike(codex, "stall_timeout_ms") ?? DEFAULT_STALL_TIMEOUT_MS
|
|
304
|
+
},
|
|
305
|
+
lifecycle: {
|
|
306
|
+
stateFieldName: readOptionalString(tracker, "state_field", env) ?? DEFAULT_WORKFLOW_TRACKER.stateFieldName,
|
|
307
|
+
activeStates,
|
|
308
|
+
terminalStates,
|
|
309
|
+
blockerCheckStates
|
|
310
|
+
},
|
|
311
|
+
format: "front-matter",
|
|
312
|
+
githubProjectId: readOptionalString(tracker, "project_id", env),
|
|
313
|
+
agentCommand: command,
|
|
314
|
+
hookPath: readOptionalString(hooks, "after_create", env),
|
|
315
|
+
maxConcurrentByState: maxConcurrentAgentsByState
|
|
316
|
+
};
|
|
317
|
+
return parsed;
|
|
318
|
+
}
|
|
319
|
+
function parseLegacyWorkflowMarkdown(markdown) {
|
|
320
|
+
const promptGuidelines = matchOptionalSection(markdown, "Prompt Guidelines") ?? "";
|
|
321
|
+
return {
|
|
322
|
+
...DEFAULT_WORKFLOW_DEFINITION,
|
|
323
|
+
promptTemplate: promptGuidelines,
|
|
324
|
+
format: "legacy-sectioned"
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
function parseFrontMatter(frontMatter) {
|
|
328
|
+
const lines = frontMatter.replace(/\r\n/g, "\n").split("\n");
|
|
329
|
+
const [value] = parseBlock(lines, 0, 0);
|
|
330
|
+
if (!value || Array.isArray(value) || typeof value !== "object") {
|
|
331
|
+
throw new Error("Workflow front matter must be a YAML object.");
|
|
332
|
+
}
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
function parseBlock(lines, startIndex, indent) {
|
|
336
|
+
let index = startIndex;
|
|
337
|
+
let collectionType = null;
|
|
338
|
+
const arrayValues = [];
|
|
339
|
+
const objectValues = {};
|
|
340
|
+
while (index < lines.length) {
|
|
341
|
+
const line = lines[index] ?? "";
|
|
342
|
+
if (!line.trim()) {
|
|
343
|
+
index += 1;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const lineIndent = countIndent(line);
|
|
347
|
+
if (lineIndent < indent) {
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
if (lineIndent > indent) {
|
|
351
|
+
throw new Error(`Invalid workflow front matter indentation near "${line.trim()}".`);
|
|
352
|
+
}
|
|
353
|
+
const trimmed = line.trim();
|
|
354
|
+
if (trimmed.startsWith("- ")) {
|
|
355
|
+
if (collectionType === "object") {
|
|
356
|
+
throw new Error("Cannot mix array and object values in workflow front matter.");
|
|
357
|
+
}
|
|
358
|
+
collectionType = "array";
|
|
359
|
+
const itemText = trimmed.slice(2).trim();
|
|
360
|
+
if (itemText === "|" || itemText === "|-") {
|
|
361
|
+
const [multiline, nextIndex3] = parseMultilineScalar(lines, index + 1, indent + 2);
|
|
362
|
+
arrayValues.push(multiline);
|
|
363
|
+
index = nextIndex3;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
if (itemText) {
|
|
367
|
+
arrayValues.push(parseScalar(itemText));
|
|
368
|
+
index += 1;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const [child2, nextIndex2] = parseBlock(lines, index + 1, indent + 2);
|
|
372
|
+
arrayValues.push(child2);
|
|
373
|
+
index = nextIndex2;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (collectionType === "array") {
|
|
377
|
+
throw new Error("Cannot mix object and array values in workflow front matter.");
|
|
378
|
+
}
|
|
379
|
+
collectionType = "object";
|
|
380
|
+
const separatorIndex = trimmed.indexOf(":");
|
|
381
|
+
if (separatorIndex < 0) {
|
|
382
|
+
throw new Error(`Invalid workflow front matter line "${trimmed}".`);
|
|
383
|
+
}
|
|
384
|
+
const key = trimmed.slice(0, separatorIndex).trim();
|
|
385
|
+
const remainder = trimmed.slice(separatorIndex + 1).trim();
|
|
386
|
+
if (remainder === "|" || remainder === "|-") {
|
|
387
|
+
const [multiline, nextIndex2] = parseMultilineScalar(lines, index + 1, indent + 2);
|
|
388
|
+
objectValues[key] = multiline;
|
|
389
|
+
index = nextIndex2;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (remainder) {
|
|
393
|
+
objectValues[key] = parseScalar(remainder);
|
|
394
|
+
index += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const [child, nextIndex] = parseBlock(lines, index + 1, indent + 2);
|
|
398
|
+
objectValues[key] = child;
|
|
399
|
+
index = nextIndex;
|
|
400
|
+
}
|
|
401
|
+
return [collectionType === "array" ? arrayValues : objectValues, index];
|
|
402
|
+
}
|
|
403
|
+
function parseMultilineScalar(lines, startIndex, indent) {
|
|
404
|
+
let index = startIndex;
|
|
405
|
+
const collected = [];
|
|
406
|
+
while (index < lines.length) {
|
|
407
|
+
const line = lines[index] ?? "";
|
|
408
|
+
if (!line.trim()) {
|
|
409
|
+
collected.push("");
|
|
410
|
+
index += 1;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const lineIndent = countIndent(line);
|
|
414
|
+
if (lineIndent < indent) {
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
collected.push(line.slice(indent));
|
|
418
|
+
index += 1;
|
|
419
|
+
}
|
|
420
|
+
return [collected.join("\n").trimEnd(), index];
|
|
421
|
+
}
|
|
422
|
+
function countIndent(line) {
|
|
423
|
+
return line.match(/^ */)?.[0].length ?? 0;
|
|
424
|
+
}
|
|
425
|
+
function parseScalar(value) {
|
|
426
|
+
if (value === "null")
|
|
427
|
+
return null;
|
|
428
|
+
if (value === "true")
|
|
429
|
+
return true;
|
|
430
|
+
if (value === "false")
|
|
431
|
+
return false;
|
|
432
|
+
if (/^-?\d+$/.test(value))
|
|
433
|
+
return Number.parseInt(value, 10);
|
|
434
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
435
|
+
return value.slice(1, -1);
|
|
436
|
+
}
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
function readObject(input, key) {
|
|
440
|
+
const value = input[key];
|
|
441
|
+
if (value === void 0 || value === null) {
|
|
442
|
+
return {};
|
|
443
|
+
}
|
|
444
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
445
|
+
throw new Error(`Workflow front matter field "${key}" must be an object.`);
|
|
446
|
+
}
|
|
447
|
+
return value;
|
|
448
|
+
}
|
|
449
|
+
function readRequiredObject(input, key) {
|
|
450
|
+
if (!(key in input)) {
|
|
451
|
+
throw new Error(`Workflow front matter field "${key}" is required.`);
|
|
452
|
+
}
|
|
453
|
+
return readObject(input, key);
|
|
454
|
+
}
|
|
455
|
+
function readOptionalString(input, key, env) {
|
|
456
|
+
const value = input[key];
|
|
457
|
+
if (value === void 0 || value === null) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
if (typeof value !== "string") {
|
|
461
|
+
throw new Error(`Workflow front matter field "${key}" must be a string.`);
|
|
462
|
+
}
|
|
463
|
+
return resolveEnvironmentValue(value, env);
|
|
464
|
+
}
|
|
465
|
+
function readOptionalWorkflowString(input, primaryKey, fallbackKey, env) {
|
|
466
|
+
return readOptionalString(input, primaryKey, env) ?? readOptionalString(input, fallbackKey, env);
|
|
467
|
+
}
|
|
468
|
+
function readRequiredString(input, key, env) {
|
|
469
|
+
const value = readOptionalString(input, key, env);
|
|
470
|
+
if (!value) {
|
|
471
|
+
throw new Error(`Workflow front matter field "${key}" is required.`);
|
|
472
|
+
}
|
|
473
|
+
return value;
|
|
474
|
+
}
|
|
475
|
+
function readStringList(input, key) {
|
|
476
|
+
const value = input[key];
|
|
477
|
+
if (value === void 0 || value === null) {
|
|
478
|
+
return void 0;
|
|
479
|
+
}
|
|
480
|
+
if (typeof value === "string") {
|
|
481
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
482
|
+
}
|
|
483
|
+
if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
|
|
484
|
+
throw new Error(`Workflow front matter field "${key}" must be an array of strings or comma-separated string.`);
|
|
485
|
+
}
|
|
486
|
+
return value;
|
|
487
|
+
}
|
|
488
|
+
function readOptionalIntegerLike(input, key) {
|
|
489
|
+
const value = input[key];
|
|
490
|
+
if (value === void 0 || value === null) {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
if (typeof value === "number") {
|
|
494
|
+
return value;
|
|
495
|
+
}
|
|
496
|
+
if (typeof value === "string" && /^-?\d+$/.test(value)) {
|
|
497
|
+
return Number.parseInt(value, 10);
|
|
498
|
+
}
|
|
499
|
+
throw new Error(`Workflow front matter field "${key}" must be an integer.`);
|
|
500
|
+
}
|
|
501
|
+
function readNumberMap(input, key) {
|
|
502
|
+
const value = input[key];
|
|
503
|
+
if (value === void 0 || value === null) {
|
|
504
|
+
return {};
|
|
505
|
+
}
|
|
506
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
507
|
+
throw new Error(`Workflow front matter field "${key}" must be an object.`);
|
|
508
|
+
}
|
|
509
|
+
const result = {};
|
|
510
|
+
for (const [entryKey, entryValue] of Object.entries(value)) {
|
|
511
|
+
if (typeof entryValue === "number") {
|
|
512
|
+
result[entryKey] = entryValue;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
if (typeof entryValue === "string" && /^\d+$/.test(entryValue)) {
|
|
516
|
+
result[entryKey] = Number.parseInt(entryValue, 10);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
throw new Error(`Workflow front matter field "${key}.${entryKey}" must be an integer.`);
|
|
520
|
+
}
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
function resolveEnvironmentValue(value, env) {
|
|
524
|
+
const envTokenMatch = value.match(/^(?:env:)?([A-Z0-9_]+)$/);
|
|
525
|
+
if (value.startsWith("env:") && envTokenMatch) {
|
|
526
|
+
const resolved = env[envTokenMatch[1]];
|
|
527
|
+
if (!resolved) {
|
|
528
|
+
throw new Error(`Workflow front matter requires environment variable ${envTokenMatch[1]}.`);
|
|
529
|
+
}
|
|
530
|
+
return resolved;
|
|
531
|
+
}
|
|
532
|
+
return value.replace(/\$\{([A-Z0-9_]+)\}/g, (_, name) => {
|
|
533
|
+
const resolved = env[name];
|
|
534
|
+
if (!resolved) {
|
|
535
|
+
throw new Error(`Workflow front matter requires environment variable ${name}.`);
|
|
536
|
+
}
|
|
537
|
+
return resolved;
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
function matchOptionalSection(markdown, heading) {
|
|
541
|
+
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
542
|
+
const pattern = new RegExp(`## ${escapedHeading}\\n\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
543
|
+
const match = markdown.match(pattern);
|
|
544
|
+
return match?.[1]?.trim() ?? null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ../core/dist/workflow/loader.js
|
|
548
|
+
import { createHash } from "crypto";
|
|
549
|
+
import { access, readFile, stat } from "fs/promises";
|
|
550
|
+
import { constants } from "fs";
|
|
551
|
+
var WorkflowConfigStore = class {
|
|
552
|
+
cache = /* @__PURE__ */ new Map();
|
|
553
|
+
async load(workflowPath, env = process.env) {
|
|
554
|
+
await access(workflowPath, constants.R_OK);
|
|
555
|
+
const fileStat = await stat(workflowPath);
|
|
556
|
+
const fingerprint = `${fileStat.mtimeMs}:${fileStat.size}`;
|
|
557
|
+
const cached = this.cache.get(workflowPath);
|
|
558
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
559
|
+
return toWorkflowResolution(workflowPath, cached.workflow, {
|
|
560
|
+
isValid: true,
|
|
561
|
+
usedLastKnownGood: false,
|
|
562
|
+
validationError: null
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
const markdown = await readFile(workflowPath, "utf8");
|
|
566
|
+
try {
|
|
567
|
+
const workflow = parseWorkflowMarkdown(markdown, env);
|
|
568
|
+
this.cache.set(workflowPath, {
|
|
569
|
+
fingerprint,
|
|
570
|
+
workflow,
|
|
571
|
+
loadedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
572
|
+
});
|
|
573
|
+
return toWorkflowResolution(workflowPath, workflow, {
|
|
574
|
+
isValid: true,
|
|
575
|
+
usedLastKnownGood: false,
|
|
576
|
+
validationError: null
|
|
577
|
+
});
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (cached) {
|
|
580
|
+
return toWorkflowResolution(workflowPath, cached.workflow, {
|
|
581
|
+
isValid: false,
|
|
582
|
+
usedLastKnownGood: true,
|
|
583
|
+
validationError: error instanceof Error ? error.message : "Invalid workflow definition."
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
throw error;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
function createDefaultWorkflowResolution() {
|
|
591
|
+
return createInvalidWorkflowResolution(null, "missing_workflow_file");
|
|
592
|
+
}
|
|
593
|
+
function createInvalidWorkflowResolution(workflowPath, validationError) {
|
|
594
|
+
return toWorkflowResolution(workflowPath, DEFAULT_WORKFLOW_DEFINITION, {
|
|
595
|
+
isValid: false,
|
|
596
|
+
usedLastKnownGood: false,
|
|
597
|
+
validationError
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function toWorkflowResolution(workflowPath, workflow, metadata) {
|
|
601
|
+
return {
|
|
602
|
+
workflowPath,
|
|
603
|
+
workflow,
|
|
604
|
+
lifecycle: workflow.lifecycle,
|
|
605
|
+
promptTemplate: workflow.promptTemplate,
|
|
606
|
+
agentCommand: workflow.agentCommand,
|
|
607
|
+
hookPath: workflow.hookPath ?? "",
|
|
608
|
+
isValid: metadata.isValid,
|
|
609
|
+
usedLastKnownGood: metadata.usedLastKnownGood,
|
|
610
|
+
validationError: metadata.validationError
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ../core/dist/workflow/render.js
|
|
615
|
+
import { Liquid, ParseError, RenderError, TokenizationError, UndefinedVariableError } from "liquidjs";
|
|
616
|
+
function buildPromptVariables(issue, options) {
|
|
617
|
+
return {
|
|
618
|
+
issue: {
|
|
619
|
+
id: issue.id,
|
|
620
|
+
identifier: issue.identifier,
|
|
621
|
+
number: issue.number,
|
|
622
|
+
title: issue.title,
|
|
623
|
+
description: issue.description,
|
|
624
|
+
priority: issue.priority,
|
|
625
|
+
url: issue.url,
|
|
626
|
+
state: issue.state,
|
|
627
|
+
labels: issue.labels,
|
|
628
|
+
blocked_by: issue.blockedBy,
|
|
629
|
+
branch_name: issue.branchName,
|
|
630
|
+
created_at: issue.createdAt,
|
|
631
|
+
updated_at: issue.updatedAt,
|
|
632
|
+
repository: `${issue.repository.owner}/${issue.repository.name}`
|
|
633
|
+
},
|
|
634
|
+
attempt: options.attempt
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
var STRICT_LIQUID_ENGINE = new Liquid({
|
|
638
|
+
strictVariables: true,
|
|
639
|
+
strictFilters: true,
|
|
640
|
+
ownPropertyOnly: true
|
|
641
|
+
});
|
|
642
|
+
function renderPrompt(template, variables, options = {}) {
|
|
643
|
+
const strict = options.strict ?? true;
|
|
644
|
+
if (!strict) {
|
|
645
|
+
return renderLegacyPrompt(template, variables);
|
|
646
|
+
}
|
|
647
|
+
try {
|
|
648
|
+
return STRICT_LIQUID_ENGINE.parseAndRenderSync(template, variables);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
throw normalizeTemplateError(error);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function normalizeTemplateError(error) {
|
|
654
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
655
|
+
if (error instanceof UndefinedVariableError || error instanceof RenderError || error instanceof ParseError && message.startsWith("undefined filter:")) {
|
|
656
|
+
return new Error(`template_render_error: ${message}`, { cause: error });
|
|
657
|
+
}
|
|
658
|
+
if (error instanceof ParseError || error instanceof TokenizationError) {
|
|
659
|
+
return new Error(`template_parse_error: ${message}`, { cause: error });
|
|
660
|
+
}
|
|
661
|
+
return new Error(`template_render_error: ${message}`, { cause: error });
|
|
662
|
+
}
|
|
663
|
+
function flattenVariables(obj, prefix = "") {
|
|
664
|
+
const result = /* @__PURE__ */ new Map();
|
|
665
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
666
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
667
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
668
|
+
for (const [nestedKey, nestedValue] of flattenVariables(value, fullKey)) {
|
|
669
|
+
result.set(nestedKey, nestedValue);
|
|
670
|
+
}
|
|
671
|
+
} else {
|
|
672
|
+
result.set(fullKey, value);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
function renderLegacyPrompt(template, variables) {
|
|
678
|
+
const flatVars = flattenVariables(variables);
|
|
679
|
+
return template.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_.]*)\}\}/g, (match, key) => {
|
|
680
|
+
const value = flatVars.get(key);
|
|
681
|
+
if (value === void 0) {
|
|
682
|
+
return match;
|
|
683
|
+
}
|
|
684
|
+
if (value === null) {
|
|
685
|
+
return "";
|
|
686
|
+
}
|
|
687
|
+
return String(value);
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// ../core/dist/orchestration/retry-policy.js
|
|
692
|
+
function calculateRetryDelay(attempt, options = {}) {
|
|
693
|
+
const baseDelayMs = options.baseDelayMs ?? DEFAULT_BASE_DELAY_MS;
|
|
694
|
+
const maxDelayMs = options.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
|
|
695
|
+
const normalizedAttempt = Math.max(1, attempt);
|
|
696
|
+
const delay2 = baseDelayMs * 2 ** (normalizedAttempt - 1);
|
|
697
|
+
return Math.min(delay2, maxDelayMs);
|
|
698
|
+
}
|
|
699
|
+
function scheduleRetryAt(now, attempt, options = {}) {
|
|
700
|
+
return new Date(now.getTime() + calculateRetryDelay(attempt, options));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// ../core/dist/workspace/env-file.js
|
|
704
|
+
import { existsSync, readFileSync } from "fs";
|
|
705
|
+
function readEnvFile(path) {
|
|
706
|
+
if (!existsSync(path)) {
|
|
707
|
+
return {};
|
|
708
|
+
}
|
|
709
|
+
return readFileSync(path, "utf8").split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).reduce((result, line) => {
|
|
710
|
+
const separatorIndex = line.indexOf("=");
|
|
711
|
+
const key = line.slice(0, separatorIndex).trim();
|
|
712
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
713
|
+
if (key) {
|
|
714
|
+
result[key] = value;
|
|
715
|
+
}
|
|
716
|
+
return result;
|
|
717
|
+
}, {});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// ../core/dist/workspace/safety.js
|
|
721
|
+
import { resolve } from "path";
|
|
722
|
+
|
|
723
|
+
// ../core/dist/workspace/identity.js
|
|
724
|
+
import { resolve as resolve2, join } from "path";
|
|
725
|
+
import { createHash as createHash2 } from "crypto";
|
|
726
|
+
function deriveIssueWorkspaceKey(identity, issueIdentifier) {
|
|
727
|
+
if (issueIdentifier) {
|
|
728
|
+
return deriveIssueWorkspaceKeyFromIdentifier(issueIdentifier);
|
|
729
|
+
}
|
|
730
|
+
return deriveLegacyIssueWorkspaceKey(identity);
|
|
731
|
+
}
|
|
732
|
+
function deriveIssueWorkspaceKeyFromIdentifier(issueIdentifier) {
|
|
733
|
+
const sanitized = issueIdentifier.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
734
|
+
if (!sanitized || /^[.]+$/.test(sanitized)) {
|
|
735
|
+
return "issue";
|
|
736
|
+
}
|
|
737
|
+
return sanitized;
|
|
738
|
+
}
|
|
739
|
+
function deriveLegacyIssueWorkspaceKey(identity) {
|
|
740
|
+
const input = [
|
|
741
|
+
identity.projectId,
|
|
742
|
+
identity.adapter,
|
|
743
|
+
identity.issueSubjectId
|
|
744
|
+
].join(":");
|
|
745
|
+
return createHash2("sha256").update(input).digest("hex").slice(0, 16);
|
|
746
|
+
}
|
|
747
|
+
function resolveIssueWorkspaceDirectory(projectDirectory, workspaceKey) {
|
|
748
|
+
const normalizedProjectDirectory = resolve2(projectDirectory);
|
|
749
|
+
const candidate = resolve2(normalizedProjectDirectory, "issues", workspaceKey);
|
|
750
|
+
if (!candidate.startsWith(`${normalizedProjectDirectory}/`)) {
|
|
751
|
+
throw new Error("Issue workspace path escapes the configured project directory.");
|
|
752
|
+
}
|
|
753
|
+
return candidate;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// ../core/dist/workspace/hooks.js
|
|
757
|
+
import { spawn } from "child_process";
|
|
758
|
+
var DEFAULT_HOOK_TIMEOUT_MS2 = 6e4;
|
|
759
|
+
async function executeHook(options) {
|
|
760
|
+
const { kind, command, cwd, env, timeoutMs } = options;
|
|
761
|
+
const start = Date.now();
|
|
762
|
+
const normalizedCommand = normalizeHookCommand(command);
|
|
763
|
+
return new Promise((resolveResult) => {
|
|
764
|
+
let timedOut = false;
|
|
765
|
+
let timer = null;
|
|
766
|
+
const child = spawn("bash", ["-lc", normalizedCommand], {
|
|
767
|
+
cwd,
|
|
768
|
+
env: { ...process.env, ...env },
|
|
769
|
+
stdio: "pipe"
|
|
770
|
+
});
|
|
771
|
+
const stderrChunks = [];
|
|
772
|
+
child.stderr?.on("data", (chunk) => {
|
|
773
|
+
stderrChunks.push(chunk);
|
|
774
|
+
});
|
|
775
|
+
if (timeoutMs > 0) {
|
|
776
|
+
timer = setTimeout(() => {
|
|
777
|
+
timedOut = true;
|
|
778
|
+
child.kill("SIGTERM");
|
|
779
|
+
setTimeout(() => {
|
|
780
|
+
try {
|
|
781
|
+
child.kill("SIGKILL");
|
|
782
|
+
} catch {
|
|
783
|
+
}
|
|
784
|
+
}, 5e3);
|
|
785
|
+
}, timeoutMs);
|
|
786
|
+
}
|
|
787
|
+
child.on("close", (code) => {
|
|
788
|
+
if (timer) {
|
|
789
|
+
clearTimeout(timer);
|
|
790
|
+
}
|
|
791
|
+
const durationMs = Date.now() - start;
|
|
792
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim();
|
|
793
|
+
if (timedOut) {
|
|
794
|
+
resolveResult({
|
|
795
|
+
kind,
|
|
796
|
+
outcome: "timeout",
|
|
797
|
+
exitCode: code,
|
|
798
|
+
durationMs,
|
|
799
|
+
error: `Hook "${kind}" timed out after ${timeoutMs}ms`
|
|
800
|
+
});
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (code !== 0) {
|
|
804
|
+
resolveResult({
|
|
805
|
+
kind,
|
|
806
|
+
outcome: "failure",
|
|
807
|
+
exitCode: code,
|
|
808
|
+
durationMs,
|
|
809
|
+
error: stderr || `Hook "${kind}" exited with code ${code}`
|
|
810
|
+
});
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
resolveResult({
|
|
814
|
+
kind,
|
|
815
|
+
outcome: "success",
|
|
816
|
+
exitCode: 0,
|
|
817
|
+
durationMs,
|
|
818
|
+
error: null
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
child.on("error", (err) => {
|
|
822
|
+
if (timer) {
|
|
823
|
+
clearTimeout(timer);
|
|
824
|
+
}
|
|
825
|
+
resolveResult({
|
|
826
|
+
kind,
|
|
827
|
+
outcome: "failure",
|
|
828
|
+
exitCode: null,
|
|
829
|
+
durationMs: Date.now() - start,
|
|
830
|
+
error: err.message
|
|
831
|
+
});
|
|
832
|
+
});
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
function buildHookEnv(context) {
|
|
836
|
+
const env = {
|
|
837
|
+
SYMPHONY_PROJECT_ID: context.projectId,
|
|
838
|
+
SYMPHONY_ISSUE_WORKSPACE_KEY: context.workspaceKey,
|
|
839
|
+
SYMPHONY_ISSUE_SUBJECT_ID: context.issueSubjectId,
|
|
840
|
+
SYMPHONY_ISSUE_IDENTIFIER: context.issueIdentifier,
|
|
841
|
+
SYMPHONY_WORKSPACE_PATH: context.workspacePath,
|
|
842
|
+
SYMPHONY_REPOSITORY_PATH: context.repositoryPath
|
|
843
|
+
};
|
|
844
|
+
if (context.runId) {
|
|
845
|
+
env.SYMPHONY_RUN_ID = context.runId;
|
|
846
|
+
}
|
|
847
|
+
if (context.state) {
|
|
848
|
+
env.SYMPHONY_ISSUE_STATE = context.state;
|
|
849
|
+
}
|
|
850
|
+
return env;
|
|
851
|
+
}
|
|
852
|
+
function resolveHookCommand(hooks, kind) {
|
|
853
|
+
switch (kind) {
|
|
854
|
+
case "after_create":
|
|
855
|
+
return hooks.afterCreate;
|
|
856
|
+
case "before_run":
|
|
857
|
+
return hooks.beforeRun;
|
|
858
|
+
case "after_run":
|
|
859
|
+
return hooks.afterRun;
|
|
860
|
+
case "before_remove":
|
|
861
|
+
return hooks.beforeRemove;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function executeWorkspaceHook(options) {
|
|
865
|
+
const hookCommand = resolveHookCommand(options.hooks, options.kind);
|
|
866
|
+
if (!hookCommand) {
|
|
867
|
+
return {
|
|
868
|
+
kind: options.kind,
|
|
869
|
+
outcome: "skipped",
|
|
870
|
+
exitCode: null,
|
|
871
|
+
durationMs: 0,
|
|
872
|
+
error: null
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
return executeHook({
|
|
876
|
+
kind: options.kind,
|
|
877
|
+
command: hookCommand,
|
|
878
|
+
cwd: options.repositoryPath,
|
|
879
|
+
env: options.env,
|
|
880
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS2
|
|
881
|
+
});
|
|
882
|
+
}
|
|
883
|
+
function normalizeHookCommand(command) {
|
|
884
|
+
const trimmed = command.trim();
|
|
885
|
+
if (trimmed.includes("/") && !trimmed.startsWith("/") && !trimmed.startsWith("./") && !trimmed.startsWith("../") && !/\s/.test(trimmed)) {
|
|
886
|
+
return `bash ./${trimmed}`;
|
|
887
|
+
}
|
|
888
|
+
return command;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// ../core/dist/observability/snapshot-builder.js
|
|
892
|
+
function buildProjectSnapshot(input) {
|
|
893
|
+
const { project, activeRuns, allRuns, summary, lastTickAt, lastError, rateLimits } = input;
|
|
894
|
+
return {
|
|
895
|
+
projectId: project.projectId,
|
|
896
|
+
slug: project.slug,
|
|
897
|
+
tracker: {
|
|
898
|
+
adapter: project.tracker.adapter,
|
|
899
|
+
bindingId: project.tracker.bindingId
|
|
900
|
+
},
|
|
901
|
+
lastTickAt,
|
|
902
|
+
health: lastError ? "degraded" : activeRuns.length > 0 ? "running" : "idle",
|
|
903
|
+
summary: {
|
|
904
|
+
dispatched: summary.dispatched,
|
|
905
|
+
suppressed: summary.suppressed,
|
|
906
|
+
recovered: summary.recovered,
|
|
907
|
+
activeRuns: activeRuns.length
|
|
908
|
+
},
|
|
909
|
+
activeRuns: activeRuns.map((run) => ({
|
|
910
|
+
runId: run.runId,
|
|
911
|
+
issueIdentifier: run.issueIdentifier,
|
|
912
|
+
issueState: run.issueState,
|
|
913
|
+
status: run.status,
|
|
914
|
+
retryKind: run.retryKind,
|
|
915
|
+
port: run.port,
|
|
916
|
+
runtimeSession: run.runtimeSession ?? null,
|
|
917
|
+
// New fields from live worker data
|
|
918
|
+
processId: run.processId ?? null,
|
|
919
|
+
turnCount: run.turnCount,
|
|
920
|
+
startedAt: run.startedAt ?? null,
|
|
921
|
+
lastEvent: run.lastEvent ?? null,
|
|
922
|
+
lastEventAt: run.lastEventAt ?? null,
|
|
923
|
+
executionPhase: run.executionPhase ?? null,
|
|
924
|
+
runPhase: run.runPhase ?? null,
|
|
925
|
+
tokenUsage: run.tokenUsage
|
|
926
|
+
})),
|
|
927
|
+
retryQueue: activeRuns.filter((run) => run.status === "retrying" && run.retryKind).map((run) => ({
|
|
928
|
+
runId: run.runId,
|
|
929
|
+
issueIdentifier: run.issueIdentifier,
|
|
930
|
+
retryKind: run.retryKind ?? "failure",
|
|
931
|
+
nextRetryAt: run.nextRetryAt
|
|
932
|
+
})),
|
|
933
|
+
lastError,
|
|
934
|
+
codexTotals: aggregateTokenUsage(allRuns ?? activeRuns, lastTickAt),
|
|
935
|
+
rateLimits: rateLimits ?? null
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
function aggregateTokenUsage(runs, lastTickAt) {
|
|
939
|
+
let inputTokens = 0;
|
|
940
|
+
let outputTokens = 0;
|
|
941
|
+
let totalTokens = 0;
|
|
942
|
+
let earliestStart = null;
|
|
943
|
+
let latestEnd = null;
|
|
944
|
+
for (const run of runs) {
|
|
945
|
+
if (run.tokenUsage) {
|
|
946
|
+
inputTokens += run.tokenUsage.inputTokens;
|
|
947
|
+
outputTokens += run.tokenUsage.outputTokens;
|
|
948
|
+
totalTokens += run.tokenUsage.totalTokens;
|
|
949
|
+
}
|
|
950
|
+
if (run.startedAt) {
|
|
951
|
+
const start = new Date(run.startedAt).getTime();
|
|
952
|
+
if (earliestStart === null || start < earliestStart) {
|
|
953
|
+
earliestStart = start;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
const end = run.completedAt ? new Date(run.completedAt).getTime() : new Date(lastTickAt).getTime();
|
|
957
|
+
if (latestEnd === null || end > latestEnd) {
|
|
958
|
+
latestEnd = end;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
const secondsRunning = earliestStart !== null && latestEnd !== null ? Math.max(0, Math.round((latestEnd - earliestStart) / 1e3)) : 0;
|
|
962
|
+
return { inputTokens, outputTokens, totalTokens, secondsRunning };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// ../core/dist/observability/fs-reader.js
|
|
966
|
+
import { readFile as readFile2, readdir } from "fs/promises";
|
|
967
|
+
async function readJsonFile(path) {
|
|
968
|
+
try {
|
|
969
|
+
const raw = await readFile2(path, "utf8");
|
|
970
|
+
return JSON.parse(raw);
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (isFileMissing(error)) {
|
|
973
|
+
return null;
|
|
974
|
+
}
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
async function safeReadDir(path) {
|
|
979
|
+
try {
|
|
980
|
+
return await readdir(path);
|
|
981
|
+
} catch (error) {
|
|
982
|
+
if (isFileMissing(error)) {
|
|
983
|
+
return [];
|
|
984
|
+
}
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function isFileMissing(error) {
|
|
989
|
+
return Boolean(error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ../core/dist/observability/event-formatter.js
|
|
993
|
+
function formatEventMessage(event) {
|
|
994
|
+
switch (event.event) {
|
|
995
|
+
case "run-dispatched":
|
|
996
|
+
return event.issueState ? `Dispatched from ${event.issueState}` : "Dispatched";
|
|
997
|
+
case "run-recovered":
|
|
998
|
+
return "Recovered existing run";
|
|
999
|
+
case "run-retried":
|
|
1000
|
+
return `Retry ${event.attempt} scheduled (${event.retryKind})`;
|
|
1001
|
+
case "run-failed":
|
|
1002
|
+
return event.lastError;
|
|
1003
|
+
case "run-suppressed":
|
|
1004
|
+
return event.reason;
|
|
1005
|
+
case "hook-executed":
|
|
1006
|
+
return `${event.hook}: ${event.outcome}`;
|
|
1007
|
+
case "hook-failed":
|
|
1008
|
+
return event.error;
|
|
1009
|
+
case "workspace-cleanup":
|
|
1010
|
+
return event.error ? `${event.outcome}: ${event.error}` : event.outcome;
|
|
1011
|
+
case "worker-error":
|
|
1012
|
+
return event.error;
|
|
1013
|
+
case "turn_started":
|
|
1014
|
+
return `Turn ${event.turnCount} started`;
|
|
1015
|
+
case "turn_completed":
|
|
1016
|
+
return `Turn ${event.turnCount} completed in ${event.durationMs}ms`;
|
|
1017
|
+
case "turn_failed":
|
|
1018
|
+
return event.error ?? `Turn ${event.turnCount} failed`;
|
|
1019
|
+
default:
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function parseRecentEvents(raw, limit, options) {
|
|
1024
|
+
const lines = raw.split("\n");
|
|
1025
|
+
if (options.allowPartialFirstLine) {
|
|
1026
|
+
lines.shift();
|
|
1027
|
+
}
|
|
1028
|
+
const events = [];
|
|
1029
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
1030
|
+
const line = lines[index]?.trim();
|
|
1031
|
+
if (!line) {
|
|
1032
|
+
continue;
|
|
1033
|
+
}
|
|
1034
|
+
const event = parseRunEventLine(line);
|
|
1035
|
+
if (!event) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
events.push({
|
|
1039
|
+
at: event.at,
|
|
1040
|
+
event: event.event,
|
|
1041
|
+
message: formatEventMessage(event)
|
|
1042
|
+
});
|
|
1043
|
+
if (events.length === limit) {
|
|
1044
|
+
break;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
return events.reverse();
|
|
1048
|
+
}
|
|
1049
|
+
function parseRunEventLine(line) {
|
|
1050
|
+
try {
|
|
1051
|
+
return JSON.parse(line);
|
|
1052
|
+
} catch {
|
|
1053
|
+
return null;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
// ../core/dist/observability/status-assembler.js
|
|
1058
|
+
function isMatchingIssueRun(run, projectId, issueId, issueIdentifier) {
|
|
1059
|
+
return Boolean(run && run.projectId === projectId && (run.issueId === issueId || run.issueIdentifier === issueIdentifier));
|
|
1060
|
+
}
|
|
1061
|
+
function mapIssueOrchestrationStateToStatus(state) {
|
|
1062
|
+
switch (state) {
|
|
1063
|
+
case "claimed":
|
|
1064
|
+
return "starting";
|
|
1065
|
+
case "running":
|
|
1066
|
+
return "running";
|
|
1067
|
+
case "retry_queued":
|
|
1068
|
+
return "retrying";
|
|
1069
|
+
case "released":
|
|
1070
|
+
return "released";
|
|
1071
|
+
case "unclaimed":
|
|
1072
|
+
return "pending";
|
|
1073
|
+
default:
|
|
1074
|
+
return state;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// ../orchestrator/dist/git.js
|
|
1079
|
+
import { spawn as spawn2 } from "child_process";
|
|
1080
|
+
import { randomUUID } from "crypto";
|
|
1081
|
+
import { access as access2, mkdir, readFile as readFile3, rename, rm, stat as stat2, writeFile } from "fs/promises";
|
|
1082
|
+
import { constants as constants2 } from "fs";
|
|
1083
|
+
import { join as join2 } from "path";
|
|
1084
|
+
var workflowConfigStore = new WorkflowConfigStore();
|
|
1085
|
+
var LOCK_RETRY_MS = 100;
|
|
1086
|
+
var LOCK_STALE_MS = 30 * 60 * 1e3;
|
|
1087
|
+
var LOCK_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
1088
|
+
async function cloneRepositoryForRun(input) {
|
|
1089
|
+
const result = await syncRepositoryForRun(input);
|
|
1090
|
+
return result.repositoryDirectory;
|
|
1091
|
+
}
|
|
1092
|
+
async function syncRepositoryForRun(input) {
|
|
1093
|
+
await mkdir(input.targetDirectory, { recursive: true });
|
|
1094
|
+
const repositoryDirectory = join2(input.targetDirectory, "repository");
|
|
1095
|
+
const lockDirectory = join2(input.targetDirectory, "repository.lock");
|
|
1096
|
+
return withRepositoryLock(lockDirectory, async () => {
|
|
1097
|
+
let hasGit = false;
|
|
1098
|
+
try {
|
|
1099
|
+
await access2(join2(repositoryDirectory, ".git"), constants2.R_OK);
|
|
1100
|
+
hasGit = true;
|
|
1101
|
+
} catch {
|
|
1102
|
+
}
|
|
1103
|
+
if (hasGit) {
|
|
1104
|
+
try {
|
|
1105
|
+
const beforeHead = await readGitHead(repositoryDirectory);
|
|
1106
|
+
await runCommand("git", [
|
|
1107
|
+
"-C",
|
|
1108
|
+
repositoryDirectory,
|
|
1109
|
+
"pull",
|
|
1110
|
+
"--ff-only"
|
|
1111
|
+
]);
|
|
1112
|
+
const afterHead = await readGitHead(repositoryDirectory);
|
|
1113
|
+
return {
|
|
1114
|
+
repositoryDirectory,
|
|
1115
|
+
changed: beforeHead !== afterHead
|
|
1116
|
+
};
|
|
1117
|
+
} catch {
|
|
1118
|
+
await rm(repositoryDirectory, { recursive: true, force: true });
|
|
1119
|
+
}
|
|
1120
|
+
} else {
|
|
1121
|
+
await rm(repositoryDirectory, { recursive: true, force: true });
|
|
1122
|
+
}
|
|
1123
|
+
const tempRepositoryDirectory = join2(input.targetDirectory, `repository.tmp-${process.pid}-${Date.now()}`);
|
|
1124
|
+
await rm(tempRepositoryDirectory, { recursive: true, force: true });
|
|
1125
|
+
try {
|
|
1126
|
+
await runCommand("git", [
|
|
1127
|
+
"clone",
|
|
1128
|
+
"--depth",
|
|
1129
|
+
"1",
|
|
1130
|
+
input.repository.cloneUrl,
|
|
1131
|
+
tempRepositoryDirectory
|
|
1132
|
+
]);
|
|
1133
|
+
await rename(tempRepositoryDirectory, repositoryDirectory);
|
|
1134
|
+
return {
|
|
1135
|
+
repositoryDirectory,
|
|
1136
|
+
changed: true
|
|
1137
|
+
};
|
|
1138
|
+
} finally {
|
|
1139
|
+
await rm(tempRepositoryDirectory, { recursive: true, force: true });
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
async function ensureIssueWorkspaceRepository(input) {
|
|
1144
|
+
return cloneRepositoryForRun({
|
|
1145
|
+
repository: input.repository,
|
|
1146
|
+
targetDirectory: input.issueWorkspacePath
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
async function loadRepositoryWorkflow(repositoryDirectory, _repository) {
|
|
1150
|
+
const workflowPath = join2(repositoryDirectory, "WORKFLOW.md");
|
|
1151
|
+
try {
|
|
1152
|
+
return await workflowConfigStore.load(workflowPath);
|
|
1153
|
+
} catch (error) {
|
|
1154
|
+
if (isMissingFileError(error)) {
|
|
1155
|
+
return createDefaultWorkflowResolution();
|
|
1156
|
+
}
|
|
1157
|
+
return createInvalidWorkflowResolution(workflowPath, error instanceof Error ? error.message : "workflow_parse_error");
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function runCommand(command, args) {
|
|
1161
|
+
return new Promise((resolve6, reject) => {
|
|
1162
|
+
const child = spawn2(command, args, {
|
|
1163
|
+
stdio: "pipe"
|
|
1164
|
+
});
|
|
1165
|
+
let stderr = "";
|
|
1166
|
+
child.stderr?.on("data", (chunk) => {
|
|
1167
|
+
stderr += String(chunk);
|
|
1168
|
+
});
|
|
1169
|
+
child.once("error", reject);
|
|
1170
|
+
child.once("exit", (code) => {
|
|
1171
|
+
if (code === 0) {
|
|
1172
|
+
resolve6();
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code ?? "unknown"}`));
|
|
1176
|
+
});
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
async function readGitHead(repositoryDirectory) {
|
|
1180
|
+
try {
|
|
1181
|
+
return await runCommandCapture("git", [
|
|
1182
|
+
"-C",
|
|
1183
|
+
repositoryDirectory,
|
|
1184
|
+
"rev-parse",
|
|
1185
|
+
"HEAD"
|
|
1186
|
+
]);
|
|
1187
|
+
} catch {
|
|
1188
|
+
return null;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function runCommandCapture(command, args) {
|
|
1192
|
+
return new Promise((resolve6, reject) => {
|
|
1193
|
+
const child = spawn2(command, args, {
|
|
1194
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1195
|
+
});
|
|
1196
|
+
let stdout = "";
|
|
1197
|
+
let stderr = "";
|
|
1198
|
+
child.stdout?.on("data", (chunk) => {
|
|
1199
|
+
stdout += String(chunk);
|
|
1200
|
+
});
|
|
1201
|
+
child.stderr?.on("data", (chunk) => {
|
|
1202
|
+
stderr += String(chunk);
|
|
1203
|
+
});
|
|
1204
|
+
child.once("error", reject);
|
|
1205
|
+
child.once("exit", (code) => {
|
|
1206
|
+
if (code === 0) {
|
|
1207
|
+
resolve6(stdout.trim());
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
reject(new Error(stderr.trim() || `${command} exited with code ${code ?? "unknown"}`));
|
|
1211
|
+
});
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
async function withRepositoryLock(lockDirectory, fn) {
|
|
1215
|
+
const ownerToken = await acquireRepositoryLock(lockDirectory);
|
|
1216
|
+
try {
|
|
1217
|
+
return await fn();
|
|
1218
|
+
} finally {
|
|
1219
|
+
await releaseRepositoryLock(lockDirectory, ownerToken);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
async function acquireRepositoryLock(lockDirectory) {
|
|
1223
|
+
const startedAt = Date.now();
|
|
1224
|
+
const ownerToken = `${process.pid}:${randomUUID()}`;
|
|
1225
|
+
for (; ; ) {
|
|
1226
|
+
try {
|
|
1227
|
+
await mkdir(lockDirectory);
|
|
1228
|
+
await writeFile(join2(lockDirectory, "owner"), `${ownerToken}
|
|
1229
|
+
${(/* @__PURE__ */ new Date()).toISOString()}
|
|
1230
|
+
`, "utf8");
|
|
1231
|
+
return ownerToken;
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
if (!isAlreadyExistsError(error)) {
|
|
1234
|
+
throw error;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
const stale = await isStaleLock(lockDirectory);
|
|
1238
|
+
if (stale) {
|
|
1239
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
if (Date.now() - startedAt >= LOCK_TIMEOUT_MS) {
|
|
1243
|
+
throw new Error(`Timed out waiting for repository cache lock: ${lockDirectory}`);
|
|
1244
|
+
}
|
|
1245
|
+
await wait(LOCK_RETRY_MS);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async function releaseRepositoryLock(lockDirectory, ownerToken) {
|
|
1249
|
+
try {
|
|
1250
|
+
const owner = await readLockOwner(lockDirectory);
|
|
1251
|
+
if (owner !== ownerToken) {
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
if (isMissingFileError(error)) {
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
throw error;
|
|
1259
|
+
}
|
|
1260
|
+
await rm(lockDirectory, { recursive: true, force: true });
|
|
1261
|
+
}
|
|
1262
|
+
async function isStaleLock(lockDirectory) {
|
|
1263
|
+
try {
|
|
1264
|
+
const details = await stat2(lockDirectory);
|
|
1265
|
+
return Date.now() - details.mtimeMs >= LOCK_STALE_MS;
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
if (isMissingFileError(error)) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
throw error;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
function isAlreadyExistsError(error) {
|
|
1274
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
|
1275
|
+
}
|
|
1276
|
+
async function readLockOwner(lockDirectory) {
|
|
1277
|
+
await access2(join2(lockDirectory, "owner"), constants2.R_OK);
|
|
1278
|
+
const owner = await readFile3(join2(lockDirectory, "owner"), "utf8");
|
|
1279
|
+
return owner.split("\n", 1)[0] || null;
|
|
1280
|
+
}
|
|
1281
|
+
function wait(ms) {
|
|
1282
|
+
return new Promise((resolve6) => {
|
|
1283
|
+
setTimeout(resolve6, ms);
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
function isMissingFileError(error) {
|
|
1287
|
+
return Boolean(error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"));
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// ../orchestrator/dist/fs-store.js
|
|
1291
|
+
import { mkdir as mkdir2, open, rename as rename2, rm as rm2, stat as stat3, writeFile as writeFile2, appendFile } from "fs/promises";
|
|
1292
|
+
import { dirname, join as join3, relative, resolve as resolve3 } from "path";
|
|
1293
|
+
var OrchestratorFsStore = class {
|
|
1294
|
+
runtimeRoot;
|
|
1295
|
+
resolvedRuntimeRoot;
|
|
1296
|
+
resolvedEventsMirrorRoot;
|
|
1297
|
+
constructor(runtimeRoot, options = {}) {
|
|
1298
|
+
this.runtimeRoot = runtimeRoot;
|
|
1299
|
+
this.resolvedRuntimeRoot = resolve3(runtimeRoot);
|
|
1300
|
+
this.resolvedEventsMirrorRoot = options.eventsMirrorRoot ? resolve3(options.eventsMirrorRoot) : null;
|
|
1301
|
+
}
|
|
1302
|
+
projectsRoot() {
|
|
1303
|
+
return join3(this.runtimeRoot, "projects");
|
|
1304
|
+
}
|
|
1305
|
+
projectDir(projectId) {
|
|
1306
|
+
return join3(this.projectsRoot(), projectId);
|
|
1307
|
+
}
|
|
1308
|
+
projectRunsDir(projectId) {
|
|
1309
|
+
return join3(this.projectDir(projectId), "runs");
|
|
1310
|
+
}
|
|
1311
|
+
runDir(runId, projectId) {
|
|
1312
|
+
if (!projectId) {
|
|
1313
|
+
return join3(this.runtimeRoot, "projects", "__unknown__", "runs", runId);
|
|
1314
|
+
}
|
|
1315
|
+
return join3(this.projectRunsDir(projectId), runId);
|
|
1316
|
+
}
|
|
1317
|
+
async loadProjectConfig(projectId) {
|
|
1318
|
+
return readJsonFile(join3(this.projectDir(projectId), "project.json"));
|
|
1319
|
+
}
|
|
1320
|
+
async saveProjectConfig(config) {
|
|
1321
|
+
await writeJsonFile(join3(this.projectDir(config.projectId), "project.json"), config);
|
|
1322
|
+
}
|
|
1323
|
+
async loadProjectIssueOrchestrations(projectId) {
|
|
1324
|
+
const issuesPath = join3(this.projectDir(projectId), "issues.json");
|
|
1325
|
+
const issues = await readJsonFile(issuesPath);
|
|
1326
|
+
if (issues) {
|
|
1327
|
+
return issues.map((issue) => ({
|
|
1328
|
+
...issue,
|
|
1329
|
+
completedOnce: issue.completedOnce ?? false
|
|
1330
|
+
}));
|
|
1331
|
+
}
|
|
1332
|
+
const legacyLeases = await readJsonFile(join3(this.projectDir(projectId), "leases.json")) ?? [];
|
|
1333
|
+
if (legacyLeases.length === 0) {
|
|
1334
|
+
return [];
|
|
1335
|
+
}
|
|
1336
|
+
const migratedIssues = legacyLeases.map((lease) => ({
|
|
1337
|
+
issueId: lease.issueId,
|
|
1338
|
+
identifier: lease.issueIdentifier,
|
|
1339
|
+
workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(lease.issueIdentifier),
|
|
1340
|
+
completedOnce: false,
|
|
1341
|
+
state: lease.status === "active" ? "claimed" : "released",
|
|
1342
|
+
currentRunId: lease.status === "active" ? lease.runId : null,
|
|
1343
|
+
retryEntry: null,
|
|
1344
|
+
updatedAt: lease.updatedAt
|
|
1345
|
+
}));
|
|
1346
|
+
await this.saveProjectIssueOrchestrations(projectId, migratedIssues);
|
|
1347
|
+
return migratedIssues;
|
|
1348
|
+
}
|
|
1349
|
+
async saveProjectIssueOrchestrations(projectId, issues) {
|
|
1350
|
+
await writeJsonFile(join3(this.projectDir(projectId), "issues.json"), issues);
|
|
1351
|
+
}
|
|
1352
|
+
async saveProjectStatus(status) {
|
|
1353
|
+
await writeJsonFile(join3(this.projectDir(status.projectId), "status.json"), status);
|
|
1354
|
+
}
|
|
1355
|
+
async loadProjectStatus(projectId) {
|
|
1356
|
+
return await readJsonFile(join3(this.projectDir(projectId), "status.json")) ?? null;
|
|
1357
|
+
}
|
|
1358
|
+
async loadRun(runId, projectId) {
|
|
1359
|
+
const runDirectory = projectId !== void 0 ? this.runDir(runId, projectId) : await this.findRunDir(runId);
|
|
1360
|
+
if (!runDirectory) {
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
return await readJsonFile(join3(runDirectory, "run.json")) ?? null;
|
|
1364
|
+
}
|
|
1365
|
+
async loadAllRuns() {
|
|
1366
|
+
const projectIds = await safeReadDir(this.projectsRoot());
|
|
1367
|
+
const runDirectories = await Promise.all(projectIds.map(async (projectId) => {
|
|
1368
|
+
const entries = await safeReadDir(this.projectRunsDir(projectId));
|
|
1369
|
+
return entries.map((entry) => this.runDir(entry, projectId));
|
|
1370
|
+
}));
|
|
1371
|
+
const runs = await Promise.all(runDirectories.flat().map((directory) => readJsonFile(join3(directory, "run.json"))));
|
|
1372
|
+
return runs.filter((run) => Boolean(run));
|
|
1373
|
+
}
|
|
1374
|
+
async saveRun(run) {
|
|
1375
|
+
await writeJsonFile(join3(this.runDir(run.runId, run.projectId), "run.json"), run);
|
|
1376
|
+
}
|
|
1377
|
+
async appendRunEvent(runId, event) {
|
|
1378
|
+
const resolvedProjectId = "projectId" in event && typeof event.projectId === "string" ? event.projectId : void 0;
|
|
1379
|
+
const runDirectory = resolvedProjectId !== void 0 ? this.runDir(runId, resolvedProjectId) : await this.findRunDir(runId);
|
|
1380
|
+
if (!runDirectory) {
|
|
1381
|
+
throw new Error(`Unable to resolve run directory for event append: ${runId}`);
|
|
1382
|
+
}
|
|
1383
|
+
const path = join3(runDirectory, "events.ndjson");
|
|
1384
|
+
const resolvedPath = resolve3(path);
|
|
1385
|
+
const serializedEvent = JSON.stringify(event) + "\n";
|
|
1386
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
1387
|
+
await appendFile(path, serializedEvent, {
|
|
1388
|
+
encoding: "utf8",
|
|
1389
|
+
mode: 420
|
|
1390
|
+
});
|
|
1391
|
+
const mirrorPath = this.resolveMirroredEventsPath(resolvedPath);
|
|
1392
|
+
if (!mirrorPath) {
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
try {
|
|
1396
|
+
await mkdir2(dirname(mirrorPath), { recursive: true });
|
|
1397
|
+
await appendFile(mirrorPath, serializedEvent, {
|
|
1398
|
+
encoding: "utf8",
|
|
1399
|
+
mode: 420
|
|
1400
|
+
});
|
|
1401
|
+
} catch (error) {
|
|
1402
|
+
console.warn(`Failed to mirror orchestrator event log to ${mirrorPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
async loadRecentRunEvents(runId, limit = 20, projectId) {
|
|
1406
|
+
const runDirectory = projectId !== void 0 ? this.runDir(runId, projectId) : await this.findRunDir(runId);
|
|
1407
|
+
if (!runDirectory) {
|
|
1408
|
+
return [];
|
|
1409
|
+
}
|
|
1410
|
+
const path = join3(runDirectory, "events.ndjson");
|
|
1411
|
+
try {
|
|
1412
|
+
if (limit <= 0) {
|
|
1413
|
+
return [];
|
|
1414
|
+
}
|
|
1415
|
+
const handle = await open(path, "r");
|
|
1416
|
+
try {
|
|
1417
|
+
const stats = await handle.stat();
|
|
1418
|
+
let position = stats.size;
|
|
1419
|
+
let tail = Buffer.alloc(0);
|
|
1420
|
+
while (position > 0) {
|
|
1421
|
+
const readSize = Math.min(position, 4096);
|
|
1422
|
+
position -= readSize;
|
|
1423
|
+
const chunk = Buffer.allocUnsafe(readSize);
|
|
1424
|
+
await handle.read(chunk, 0, readSize, position);
|
|
1425
|
+
tail = Buffer.concat([chunk, tail]);
|
|
1426
|
+
const events = parseRecentEvents(tail.toString("utf8"), limit, {
|
|
1427
|
+
allowPartialFirstLine: position > 0
|
|
1428
|
+
});
|
|
1429
|
+
if (events.length >= limit) {
|
|
1430
|
+
return events;
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
return parseRecentEvents(tail.toString("utf8"), limit, {
|
|
1434
|
+
allowPartialFirstLine: false
|
|
1435
|
+
});
|
|
1436
|
+
} finally {
|
|
1437
|
+
await handle.close();
|
|
1438
|
+
}
|
|
1439
|
+
} catch (error) {
|
|
1440
|
+
if (isFileMissing(error)) {
|
|
1441
|
+
return [];
|
|
1442
|
+
}
|
|
1443
|
+
throw error;
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
issueWorkspaceDir(projectId, workspaceKey) {
|
|
1447
|
+
return join3(this.projectDir(projectId), "issues", workspaceKey);
|
|
1448
|
+
}
|
|
1449
|
+
async loadIssueWorkspace(projectId, workspaceKey) {
|
|
1450
|
+
return await readJsonFile(join3(this.issueWorkspaceDir(projectId, workspaceKey), "workspace.json")) ?? null;
|
|
1451
|
+
}
|
|
1452
|
+
async loadIssueWorkspaces(projectId) {
|
|
1453
|
+
const issuesDir = join3(this.projectDir(projectId), "issues");
|
|
1454
|
+
const entries = await safeReadDir(issuesDir);
|
|
1455
|
+
const records = await Promise.all(entries.map((entry) => this.loadIssueWorkspace(projectId, entry)));
|
|
1456
|
+
return records.filter((record) => Boolean(record));
|
|
1457
|
+
}
|
|
1458
|
+
async saveIssueWorkspace(record) {
|
|
1459
|
+
await writeJsonFile(join3(this.issueWorkspaceDir(record.projectId, record.workspaceKey), "workspace.json"), record);
|
|
1460
|
+
}
|
|
1461
|
+
async removeIssueWorkspace(projectId, workspaceKey) {
|
|
1462
|
+
const dir = this.issueWorkspaceDir(projectId, workspaceKey);
|
|
1463
|
+
await rm2(dir, { recursive: true, force: true });
|
|
1464
|
+
}
|
|
1465
|
+
async findRunDir(runId) {
|
|
1466
|
+
const projectIds = await safeReadDir(this.projectsRoot());
|
|
1467
|
+
for (const projectId of projectIds) {
|
|
1468
|
+
const candidate = this.runDir(runId, projectId);
|
|
1469
|
+
const run = await readJsonFile(join3(candidate, "run.json"));
|
|
1470
|
+
if (run || await pathExists(join3(candidate, "events.ndjson"))) {
|
|
1471
|
+
return candidate;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
return null;
|
|
1475
|
+
}
|
|
1476
|
+
resolveMirroredEventsPath(primaryPath) {
|
|
1477
|
+
if (!this.resolvedEventsMirrorRoot) {
|
|
1478
|
+
return null;
|
|
1479
|
+
}
|
|
1480
|
+
const relativePath = relative(this.resolvedRuntimeRoot, primaryPath);
|
|
1481
|
+
if (relativePath.startsWith("..")) {
|
|
1482
|
+
return null;
|
|
1483
|
+
}
|
|
1484
|
+
const mirrorPath = join3(this.resolvedEventsMirrorRoot, relativePath);
|
|
1485
|
+
return mirrorPath === primaryPath ? null : mirrorPath;
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
async function writeJsonFile(path, value) {
|
|
1489
|
+
await mkdir2(dirname(path), { recursive: true });
|
|
1490
|
+
const temporaryPath = `${path}.tmp`;
|
|
1491
|
+
await writeFile2(temporaryPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
1492
|
+
await rename2(temporaryPath, path);
|
|
1493
|
+
}
|
|
1494
|
+
async function pathExists(path) {
|
|
1495
|
+
try {
|
|
1496
|
+
await stat3(path);
|
|
1497
|
+
return true;
|
|
1498
|
+
} catch (error) {
|
|
1499
|
+
if (isFileMissing(error)) {
|
|
1500
|
+
return false;
|
|
1501
|
+
}
|
|
1502
|
+
throw error;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
// ../tracker-github/dist/adapter.js
|
|
1507
|
+
var DEFAULT_API_URL = "https://api.github.com/graphql";
|
|
1508
|
+
var DEFAULT_PAGE_SIZE = 25;
|
|
1509
|
+
var DEFAULT_NETWORK_TIMEOUT_MS = 3e4;
|
|
1510
|
+
var GitHubTrackerError = class extends Error {
|
|
1511
|
+
};
|
|
1512
|
+
var GitHubTrackerHttpError = class extends GitHubTrackerError {
|
|
1513
|
+
status;
|
|
1514
|
+
details;
|
|
1515
|
+
constructor(message, status, details) {
|
|
1516
|
+
super(message);
|
|
1517
|
+
this.status = status;
|
|
1518
|
+
this.details = details;
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1521
|
+
var GitHubTrackerQueryError = class extends GitHubTrackerError {
|
|
1522
|
+
};
|
|
1523
|
+
function normalizeProjectItem(projectId, item, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, priority = {}, rateLimits = null) {
|
|
1524
|
+
if (item.content?.__typename !== "Issue") {
|
|
1525
|
+
return null;
|
|
1526
|
+
}
|
|
1527
|
+
const fieldValues = extractFieldValues(item.fieldValues?.nodes ?? []);
|
|
1528
|
+
const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
|
|
1529
|
+
const repository = item.content.repository;
|
|
1530
|
+
const blockedBy = (item.content.blockedBy?.nodes ?? []).flatMap((node) => node ? [
|
|
1531
|
+
{
|
|
1532
|
+
id: node.id,
|
|
1533
|
+
identifier: `${node.repository.owner.login}/${node.repository.name}#${node.number}`,
|
|
1534
|
+
state: normalizeBlockerState(node.state, lifecycle)
|
|
1535
|
+
}
|
|
1536
|
+
] : []);
|
|
1537
|
+
return {
|
|
1538
|
+
id: item.content.id,
|
|
1539
|
+
identifier: `${repository.owner.login}/${repository.name}#${item.content.number}`,
|
|
1540
|
+
number: item.content.number,
|
|
1541
|
+
title: item.content.title,
|
|
1542
|
+
description: item.content.body,
|
|
1543
|
+
priority: resolvePriority(item, priority),
|
|
1544
|
+
state,
|
|
1545
|
+
branchName: null,
|
|
1546
|
+
url: item.content.url,
|
|
1547
|
+
labels: (item.content.labels?.nodes ?? []).flatMap((label) => label?.name ? [label.name.toLowerCase()] : []).sort(),
|
|
1548
|
+
blockedBy,
|
|
1549
|
+
createdAt: item.content.createdAt,
|
|
1550
|
+
updatedAt: item.content.updatedAt ?? item.updatedAt,
|
|
1551
|
+
repository: {
|
|
1552
|
+
owner: repository.owner.login,
|
|
1553
|
+
name: repository.name,
|
|
1554
|
+
url: repository.url,
|
|
1555
|
+
cloneUrl: deriveCloneUrl(repository.url)
|
|
1556
|
+
},
|
|
1557
|
+
tracker: {
|
|
1558
|
+
adapter: "github-project",
|
|
1559
|
+
bindingId: projectId,
|
|
1560
|
+
itemId: item.id
|
|
1561
|
+
},
|
|
1562
|
+
metadata: fieldValues,
|
|
1563
|
+
rateLimits
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
async function fetchProjectIssues(config, fetchImpl = fetch) {
|
|
1567
|
+
const issues = [];
|
|
1568
|
+
let cursor = null;
|
|
1569
|
+
const priorityOptionIds = config.priorityFieldName ? await fetchPriorityOptionOrder(config, config.priorityFieldName, fetchImpl) : void 0;
|
|
1570
|
+
const currentUserLogin = config.assignedOnly ? await fetchCurrentUserLogin(config, fetchImpl) : null;
|
|
1571
|
+
let excludedCount = 0;
|
|
1572
|
+
let latestRateLimits = null;
|
|
1573
|
+
do {
|
|
1574
|
+
const pageResult = await fetchProjectItemsPage(config, cursor, fetchImpl);
|
|
1575
|
+
const page = pageResult.page;
|
|
1576
|
+
latestRateLimits = pageResult.rateLimits ?? latestRateLimits;
|
|
1577
|
+
const pageIssues = (page.nodes ?? []).flatMap((item) => {
|
|
1578
|
+
if (!item) {
|
|
1579
|
+
return [];
|
|
1580
|
+
}
|
|
1581
|
+
const normalized = normalizeProjectItem(config.projectId, item, config.lifecycle, {
|
|
1582
|
+
fieldName: config.priorityFieldName,
|
|
1583
|
+
optionIds: priorityOptionIds
|
|
1584
|
+
}, latestRateLimits);
|
|
1585
|
+
if (!normalized) {
|
|
1586
|
+
return [];
|
|
1587
|
+
}
|
|
1588
|
+
if (currentUserLogin && !isIssueAssignedToLogin(item, currentUserLogin)) {
|
|
1589
|
+
excludedCount += 1;
|
|
1590
|
+
return [];
|
|
1591
|
+
}
|
|
1592
|
+
return [normalized];
|
|
1593
|
+
});
|
|
1594
|
+
issues.push(...pageIssues);
|
|
1595
|
+
cursor = page.pageInfo.hasNextPage ? page.pageInfo.endCursor : null;
|
|
1596
|
+
} while (cursor);
|
|
1597
|
+
if (currentUserLogin) {
|
|
1598
|
+
emitAssignedOnlyFilterEvent({
|
|
1599
|
+
projectId: config.projectId,
|
|
1600
|
+
currentUserLogin,
|
|
1601
|
+
includedCount: issues.length,
|
|
1602
|
+
excludedCount
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
if (latestRateLimits) {
|
|
1606
|
+
for (const issue of issues) {
|
|
1607
|
+
issue.rateLimits = latestRateLimits;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
return issues;
|
|
1611
|
+
}
|
|
1612
|
+
async function fetchIssueStatesByIds(config, issueIds, fetchImpl = fetch) {
|
|
1613
|
+
if (issueIds.length === 0) {
|
|
1614
|
+
return [];
|
|
1615
|
+
}
|
|
1616
|
+
const issues = [];
|
|
1617
|
+
for (const issueIdBatch of chunkValues([...new Set(issueIds)], 100)) {
|
|
1618
|
+
const result = await executeGraphQLQueryWithMetadata(config, ISSUE_STATES_BY_IDS_QUERY, {
|
|
1619
|
+
issueIds: issueIdBatch
|
|
1620
|
+
}, fetchImpl);
|
|
1621
|
+
const data = result.data;
|
|
1622
|
+
const rateLimits = result.rateLimits;
|
|
1623
|
+
for (const node of data.nodes ?? []) {
|
|
1624
|
+
const projectItem = await resolveIssueProjectItemForStateLookup(config, node, fetchImpl);
|
|
1625
|
+
const normalized = normalizeIssueStateLookupNode(config.projectId, node, projectItem, config.lifecycle, rateLimits);
|
|
1626
|
+
if (normalized) {
|
|
1627
|
+
issues.push(normalized);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
return issues;
|
|
1632
|
+
}
|
|
1633
|
+
async function fetchProjectItemsPage(config, cursor, fetchImpl) {
|
|
1634
|
+
const result = await executeGraphQLQueryWithMetadata(config, PROJECT_ITEMS_QUERY, {
|
|
1635
|
+
projectId: config.projectId,
|
|
1636
|
+
cursor,
|
|
1637
|
+
pageSize: config.pageSize ?? DEFAULT_PAGE_SIZE
|
|
1638
|
+
}, fetchImpl);
|
|
1639
|
+
const data = result.data;
|
|
1640
|
+
const items = data.node?.items;
|
|
1641
|
+
if (!items) {
|
|
1642
|
+
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include project items.");
|
|
1643
|
+
}
|
|
1644
|
+
return {
|
|
1645
|
+
page: items,
|
|
1646
|
+
rateLimits: result.rateLimits
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
var fetchGithubProjectIssues = fetchProjectIssues;
|
|
1650
|
+
var fetchGithubIssueStatesByIds = fetchIssueStatesByIds;
|
|
1651
|
+
async function fetchCurrentUserLogin(config, fetchImpl) {
|
|
1652
|
+
const response = await fetchImpl(resolveRestUserApiUrl(config.apiUrl), {
|
|
1653
|
+
method: "GET",
|
|
1654
|
+
headers: {
|
|
1655
|
+
authorization: `Bearer ${config.token}`,
|
|
1656
|
+
"user-agent": "gh-symphony",
|
|
1657
|
+
accept: "application/vnd.github+json"
|
|
1658
|
+
},
|
|
1659
|
+
signal: buildRequestSignal(config.timeoutMs)
|
|
1660
|
+
});
|
|
1661
|
+
if (!response.ok) {
|
|
1662
|
+
const details = await response.text();
|
|
1663
|
+
throw new GitHubTrackerHttpError(`GitHub REST request failed with status ${response.status}`, response.status, details);
|
|
1664
|
+
}
|
|
1665
|
+
const payload = await response.json();
|
|
1666
|
+
if (!payload.login) {
|
|
1667
|
+
throw new GitHubTrackerQueryError("GitHub REST response did not include the authenticated user login.");
|
|
1668
|
+
}
|
|
1669
|
+
return payload.login;
|
|
1670
|
+
}
|
|
1671
|
+
function isIssueAssignedToLogin(item, login) {
|
|
1672
|
+
if (item.content?.__typename !== "Issue") {
|
|
1673
|
+
return false;
|
|
1674
|
+
}
|
|
1675
|
+
return (item.content.assignees?.nodes ?? []).some((assignee) => assignee?.login === login);
|
|
1676
|
+
}
|
|
1677
|
+
function emitAssignedOnlyFilterEvent(input) {
|
|
1678
|
+
console.info(JSON.stringify({
|
|
1679
|
+
event: "tracker-assigned-only-filtered",
|
|
1680
|
+
projectId: input.projectId,
|
|
1681
|
+
currentUserLogin: input.currentUserLogin,
|
|
1682
|
+
includedCount: input.includedCount,
|
|
1683
|
+
excludedCount: input.excludedCount
|
|
1684
|
+
}));
|
|
1685
|
+
}
|
|
1686
|
+
function extractFieldValues(nodes) {
|
|
1687
|
+
return nodes.reduce((values, node) => {
|
|
1688
|
+
const fieldName = node?.field?.name;
|
|
1689
|
+
if (!fieldName) {
|
|
1690
|
+
return values;
|
|
1691
|
+
}
|
|
1692
|
+
if (node.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.name) {
|
|
1693
|
+
values[fieldName] = node.name;
|
|
1694
|
+
}
|
|
1695
|
+
if (node.__typename === "ProjectV2ItemFieldTextValue" && node.text) {
|
|
1696
|
+
values[fieldName] = node.text;
|
|
1697
|
+
}
|
|
1698
|
+
return values;
|
|
1699
|
+
}, {});
|
|
1700
|
+
}
|
|
1701
|
+
function normalizeIssueStateLookupNode(projectId, issue, projectItem, lifecycle = DEFAULT_WORKFLOW_LIFECYCLE, rateLimits = null) {
|
|
1702
|
+
if (issue?.__typename !== "Issue") {
|
|
1703
|
+
return null;
|
|
1704
|
+
}
|
|
1705
|
+
if (!projectItem) {
|
|
1706
|
+
return null;
|
|
1707
|
+
}
|
|
1708
|
+
const fieldValues = extractFieldValues(projectItem.fieldValues?.nodes ?? []);
|
|
1709
|
+
const state = fieldValues[lifecycle.stateFieldName] ?? "Unknown";
|
|
1710
|
+
const repository = issue.repository;
|
|
1711
|
+
const identifier = `${repository.owner.login}/${repository.name}#${issue.number}`;
|
|
1712
|
+
return {
|
|
1713
|
+
id: issue.id,
|
|
1714
|
+
identifier,
|
|
1715
|
+
number: issue.number,
|
|
1716
|
+
title: identifier,
|
|
1717
|
+
description: null,
|
|
1718
|
+
priority: null,
|
|
1719
|
+
state,
|
|
1720
|
+
branchName: null,
|
|
1721
|
+
url: `${repository.url}/issues/${issue.number}`,
|
|
1722
|
+
labels: [],
|
|
1723
|
+
blockedBy: [],
|
|
1724
|
+
createdAt: null,
|
|
1725
|
+
updatedAt: projectItem.updatedAt ?? issue.updatedAt,
|
|
1726
|
+
repository: {
|
|
1727
|
+
owner: repository.owner.login,
|
|
1728
|
+
name: repository.name,
|
|
1729
|
+
url: repository.url,
|
|
1730
|
+
cloneUrl: deriveCloneUrl(repository.url)
|
|
1731
|
+
},
|
|
1732
|
+
tracker: {
|
|
1733
|
+
adapter: "github-project",
|
|
1734
|
+
bindingId: projectId,
|
|
1735
|
+
itemId: projectItem.id
|
|
1736
|
+
},
|
|
1737
|
+
metadata: fieldValues,
|
|
1738
|
+
rateLimits
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
async function resolveIssueProjectItemForStateLookup(config, issue, fetchImpl) {
|
|
1742
|
+
if (issue?.__typename !== "Issue") {
|
|
1743
|
+
return null;
|
|
1744
|
+
}
|
|
1745
|
+
let connection = issue.projectItems;
|
|
1746
|
+
let projectItem = findProjectItemByProjectId(connection?.nodes ?? [], config.projectId);
|
|
1747
|
+
let cursor = connection?.pageInfo.endCursor ?? null;
|
|
1748
|
+
while (!projectItem && connection?.pageInfo.hasNextPage) {
|
|
1749
|
+
const nextPage = await fetchIssueProjectItemsPage(config, issue.id, cursor, fetchImpl);
|
|
1750
|
+
projectItem = findProjectItemByProjectId(nextPage.nodes ?? [], config.projectId);
|
|
1751
|
+
connection = nextPage;
|
|
1752
|
+
cursor = nextPage.pageInfo.endCursor;
|
|
1753
|
+
}
|
|
1754
|
+
return projectItem;
|
|
1755
|
+
}
|
|
1756
|
+
async function fetchIssueProjectItemsPage(config, issueId, cursor, fetchImpl) {
|
|
1757
|
+
const result = await executeGraphQLQueryWithMetadata(config, ISSUE_PROJECT_ITEMS_PAGE_QUERY, {
|
|
1758
|
+
issueId,
|
|
1759
|
+
cursor
|
|
1760
|
+
}, fetchImpl);
|
|
1761
|
+
const data = result.data;
|
|
1762
|
+
const issue = data.node;
|
|
1763
|
+
if (issue?.__typename !== "Issue" || !issue.projectItems) {
|
|
1764
|
+
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include issue project items.");
|
|
1765
|
+
}
|
|
1766
|
+
return issue.projectItems;
|
|
1767
|
+
}
|
|
1768
|
+
function findProjectItemByProjectId(nodes, projectId) {
|
|
1769
|
+
return nodes.find((item) => item?.project?.id === projectId) ?? null;
|
|
1770
|
+
}
|
|
1771
|
+
function resolvePriority(item, priority) {
|
|
1772
|
+
if (!priority.fieldName || !priority.optionIds) {
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
for (const node of item.fieldValues?.nodes ?? []) {
|
|
1776
|
+
if (node?.__typename === "ProjectV2ItemFieldSingleSelectValue" && node.field?.name === priority.fieldName && node.optionId) {
|
|
1777
|
+
return priority.optionIds[node.optionId] ?? null;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
function extractPriorityOptionOrder(fields, priorityFieldName) {
|
|
1783
|
+
for (const field of fields) {
|
|
1784
|
+
if (isSingleSelectProjectField(field) && field.name === priorityFieldName) {
|
|
1785
|
+
let nextPriority = 0;
|
|
1786
|
+
const optionEntries = (field.options ?? []).flatMap((option) => {
|
|
1787
|
+
if (!option?.id) {
|
|
1788
|
+
return [];
|
|
1789
|
+
}
|
|
1790
|
+
const entry = [option.id, nextPriority];
|
|
1791
|
+
nextPriority += 1;
|
|
1792
|
+
return [entry];
|
|
1793
|
+
});
|
|
1794
|
+
return Object.fromEntries(optionEntries);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return void 0;
|
|
1798
|
+
}
|
|
1799
|
+
async function fetchPriorityOptionOrder(config, priorityFieldName, fetchImpl) {
|
|
1800
|
+
const data = await executeGraphQLQuery(config, PROJECT_FIELDS_QUERY, { projectId: config.projectId }, fetchImpl);
|
|
1801
|
+
return extractPriorityOptionOrder(data.node?.fields?.nodes ?? [], priorityFieldName);
|
|
1802
|
+
}
|
|
1803
|
+
function isSingleSelectProjectField(field) {
|
|
1804
|
+
return field?.__typename === "ProjectV2SingleSelectField";
|
|
1805
|
+
}
|
|
1806
|
+
function deriveCloneUrl(repositoryUrl) {
|
|
1807
|
+
if (repositoryUrl.startsWith("file://") || repositoryUrl.endsWith(".git")) {
|
|
1808
|
+
return repositoryUrl;
|
|
1809
|
+
}
|
|
1810
|
+
return `${repositoryUrl}.git`;
|
|
1811
|
+
}
|
|
1812
|
+
function normalizeBlockerState(state, lifecycle) {
|
|
1813
|
+
if (!state) {
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
const normalized = state.trim().toLowerCase();
|
|
1817
|
+
if (normalized === "closed") {
|
|
1818
|
+
return lifecycle.terminalStates[0] ?? state;
|
|
1819
|
+
}
|
|
1820
|
+
if (normalized === "open") {
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
return state;
|
|
1824
|
+
}
|
|
1825
|
+
function resolveRestUserApiUrl(apiUrl) {
|
|
1826
|
+
const parsed = new URL(apiUrl ?? DEFAULT_API_URL);
|
|
1827
|
+
const pathSegments = parsed.pathname.split("/").filter(Boolean);
|
|
1828
|
+
if (pathSegments.at(-1) === "graphql") {
|
|
1829
|
+
pathSegments.pop();
|
|
1830
|
+
}
|
|
1831
|
+
parsed.pathname = `/${pathSegments.join("/")}/user`.replace(/\/{2,}/g, "/");
|
|
1832
|
+
parsed.search = "";
|
|
1833
|
+
parsed.hash = "";
|
|
1834
|
+
return parsed.toString();
|
|
1835
|
+
}
|
|
1836
|
+
function chunkValues(values, size) {
|
|
1837
|
+
const chunks = [];
|
|
1838
|
+
for (let index = 0; index < values.length; index += size) {
|
|
1839
|
+
chunks.push(values.slice(index, index + size));
|
|
1840
|
+
}
|
|
1841
|
+
return chunks;
|
|
1842
|
+
}
|
|
1843
|
+
function buildRequestSignal(timeoutMs) {
|
|
1844
|
+
return AbortSignal.timeout(resolveNetworkTimeoutMs(timeoutMs));
|
|
1845
|
+
}
|
|
1846
|
+
function resolveNetworkTimeoutMs(timeoutMs) {
|
|
1847
|
+
if (timeoutMs !== void 0 && Number.isInteger(timeoutMs) && timeoutMs > 0) {
|
|
1848
|
+
return timeoutMs;
|
|
1849
|
+
}
|
|
1850
|
+
return DEFAULT_NETWORK_TIMEOUT_MS;
|
|
1851
|
+
}
|
|
1852
|
+
async function executeGraphQLQuery(config, query, variables, fetchImpl) {
|
|
1853
|
+
const result = await executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl);
|
|
1854
|
+
return result.data;
|
|
1855
|
+
}
|
|
1856
|
+
async function executeGraphQLQueryWithMetadata(config, query, variables, fetchImpl) {
|
|
1857
|
+
const response = await fetchImpl(config.apiUrl ?? DEFAULT_API_URL, {
|
|
1858
|
+
method: "POST",
|
|
1859
|
+
headers: {
|
|
1860
|
+
"content-type": "application/json",
|
|
1861
|
+
authorization: `Bearer ${config.token}`
|
|
1862
|
+
},
|
|
1863
|
+
body: JSON.stringify({
|
|
1864
|
+
query,
|
|
1865
|
+
variables
|
|
1866
|
+
}),
|
|
1867
|
+
signal: buildRequestSignal(config.timeoutMs)
|
|
1868
|
+
});
|
|
1869
|
+
if (!response.ok) {
|
|
1870
|
+
const details = await response.text();
|
|
1871
|
+
throw new GitHubTrackerHttpError(`GitHub GraphQL request failed with status ${response.status}`, response.status, details);
|
|
1872
|
+
}
|
|
1873
|
+
const payload = await response.json();
|
|
1874
|
+
if (payload.errors?.length) {
|
|
1875
|
+
throw new GitHubTrackerQueryError(payload.errors.map((error) => error.message).join("; "));
|
|
1876
|
+
}
|
|
1877
|
+
if (!payload.data) {
|
|
1878
|
+
throw new GitHubTrackerQueryError("GitHub GraphQL response did not include data.");
|
|
1879
|
+
}
|
|
1880
|
+
const data = payload.data;
|
|
1881
|
+
return {
|
|
1882
|
+
data,
|
|
1883
|
+
rateLimits: extractGitHubRateLimits(response.headers)
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
function extractGitHubRateLimits(headers) {
|
|
1887
|
+
if (!headers || typeof headers.get !== "function") {
|
|
1888
|
+
return null;
|
|
1889
|
+
}
|
|
1890
|
+
const limit = parseIntegerHeader(headers.get("x-ratelimit-limit"));
|
|
1891
|
+
const remaining = parseIntegerHeader(headers.get("x-ratelimit-remaining"));
|
|
1892
|
+
const used = parseIntegerHeader(headers.get("x-ratelimit-used"));
|
|
1893
|
+
const reset = parseIntegerHeader(headers.get("x-ratelimit-reset"));
|
|
1894
|
+
const resource = headers.get("x-ratelimit-resource");
|
|
1895
|
+
if (limit === null && remaining === null && used === null && reset === null && resource === null) {
|
|
1896
|
+
return null;
|
|
1897
|
+
}
|
|
1898
|
+
return {
|
|
1899
|
+
source: "github",
|
|
1900
|
+
limit,
|
|
1901
|
+
remaining,
|
|
1902
|
+
used,
|
|
1903
|
+
reset,
|
|
1904
|
+
resetAt: reset === null ? null : new Date(reset * 1e3).toISOString(),
|
|
1905
|
+
resource
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
function parseIntegerHeader(value) {
|
|
1909
|
+
if (value === null) {
|
|
1910
|
+
return null;
|
|
1911
|
+
}
|
|
1912
|
+
const parsed = Number.parseInt(value, 10);
|
|
1913
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1914
|
+
}
|
|
1915
|
+
var PROJECT_ITEMS_QUERY = `
|
|
1916
|
+
query ProjectItems($projectId: ID!, $cursor: String, $pageSize: Int!) {
|
|
1917
|
+
node(id: $projectId) {
|
|
1918
|
+
__typename
|
|
1919
|
+
... on ProjectV2 {
|
|
1920
|
+
items(first: $pageSize, after: $cursor) {
|
|
1921
|
+
nodes {
|
|
1922
|
+
id
|
|
1923
|
+
updatedAt
|
|
1924
|
+
fieldValues(first: 20) {
|
|
1925
|
+
nodes {
|
|
1926
|
+
__typename
|
|
1927
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
1928
|
+
name
|
|
1929
|
+
optionId
|
|
1930
|
+
field {
|
|
1931
|
+
... on ProjectV2SingleSelectField {
|
|
1932
|
+
name
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
... on ProjectV2ItemFieldTextValue {
|
|
1937
|
+
text
|
|
1938
|
+
field {
|
|
1939
|
+
... on ProjectV2FieldCommon {
|
|
1940
|
+
name
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
content {
|
|
1947
|
+
__typename
|
|
1948
|
+
... on Issue {
|
|
1949
|
+
id
|
|
1950
|
+
number
|
|
1951
|
+
title
|
|
1952
|
+
body
|
|
1953
|
+
url
|
|
1954
|
+
createdAt
|
|
1955
|
+
updatedAt
|
|
1956
|
+
labels(first: 20) {
|
|
1957
|
+
nodes {
|
|
1958
|
+
name
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
assignees(first: 20) {
|
|
1962
|
+
nodes {
|
|
1963
|
+
login
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
repository {
|
|
1967
|
+
name
|
|
1968
|
+
url
|
|
1969
|
+
owner {
|
|
1970
|
+
login
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
blockedBy(first: 100) {
|
|
1974
|
+
nodes {
|
|
1975
|
+
id
|
|
1976
|
+
number
|
|
1977
|
+
state
|
|
1978
|
+
repository {
|
|
1979
|
+
name
|
|
1980
|
+
owner {
|
|
1981
|
+
login
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
pageInfo {
|
|
1990
|
+
endCursor
|
|
1991
|
+
hasNextPage
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
`;
|
|
1998
|
+
var PROJECT_FIELDS_QUERY = `
|
|
1999
|
+
query ProjectFields($projectId: ID!) {
|
|
2000
|
+
node(id: $projectId) {
|
|
2001
|
+
__typename
|
|
2002
|
+
... on ProjectV2 {
|
|
2003
|
+
fields(first: 100) {
|
|
2004
|
+
nodes {
|
|
2005
|
+
__typename
|
|
2006
|
+
... on ProjectV2SingleSelectField {
|
|
2007
|
+
name
|
|
2008
|
+
options {
|
|
2009
|
+
id
|
|
2010
|
+
name
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
`;
|
|
2019
|
+
var ISSUE_STATES_BY_IDS_QUERY = `
|
|
2020
|
+
query IssueStatesByIds($issueIds: [ID!]!) {
|
|
2021
|
+
nodes(ids: $issueIds) {
|
|
2022
|
+
__typename
|
|
2023
|
+
... on Issue {
|
|
2024
|
+
id
|
|
2025
|
+
number
|
|
2026
|
+
updatedAt
|
|
2027
|
+
repository {
|
|
2028
|
+
name
|
|
2029
|
+
url
|
|
2030
|
+
owner {
|
|
2031
|
+
login
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
projectItems(first: 100, includeArchived: false) {
|
|
2035
|
+
nodes {
|
|
2036
|
+
id
|
|
2037
|
+
updatedAt
|
|
2038
|
+
project {
|
|
2039
|
+
id
|
|
2040
|
+
}
|
|
2041
|
+
fieldValues(first: 20) {
|
|
2042
|
+
nodes {
|
|
2043
|
+
__typename
|
|
2044
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2045
|
+
name
|
|
2046
|
+
optionId
|
|
2047
|
+
field {
|
|
2048
|
+
... on ProjectV2SingleSelectField {
|
|
2049
|
+
name
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
... on ProjectV2ItemFieldTextValue {
|
|
2054
|
+
text
|
|
2055
|
+
field {
|
|
2056
|
+
... on ProjectV2FieldCommon {
|
|
2057
|
+
name
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
pageInfo {
|
|
2065
|
+
endCursor
|
|
2066
|
+
hasNextPage
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
`;
|
|
2073
|
+
var ISSUE_PROJECT_ITEMS_PAGE_QUERY = `
|
|
2074
|
+
query IssueProjectItemsPage($issueId: ID!, $cursor: String) {
|
|
2075
|
+
node(id: $issueId) {
|
|
2076
|
+
__typename
|
|
2077
|
+
... on Issue {
|
|
2078
|
+
id
|
|
2079
|
+
number
|
|
2080
|
+
updatedAt
|
|
2081
|
+
repository {
|
|
2082
|
+
name
|
|
2083
|
+
url
|
|
2084
|
+
owner {
|
|
2085
|
+
login
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
projectItems(first: 100, after: $cursor, includeArchived: false) {
|
|
2089
|
+
nodes {
|
|
2090
|
+
id
|
|
2091
|
+
updatedAt
|
|
2092
|
+
project {
|
|
2093
|
+
id
|
|
2094
|
+
}
|
|
2095
|
+
fieldValues(first: 20) {
|
|
2096
|
+
nodes {
|
|
2097
|
+
__typename
|
|
2098
|
+
... on ProjectV2ItemFieldSingleSelectValue {
|
|
2099
|
+
name
|
|
2100
|
+
optionId
|
|
2101
|
+
field {
|
|
2102
|
+
... on ProjectV2SingleSelectField {
|
|
2103
|
+
name
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
... on ProjectV2ItemFieldTextValue {
|
|
2108
|
+
text
|
|
2109
|
+
field {
|
|
2110
|
+
... on ProjectV2FieldCommon {
|
|
2111
|
+
name
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
pageInfo {
|
|
2119
|
+
endCursor
|
|
2120
|
+
hasNextPage
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
`;
|
|
2127
|
+
|
|
2128
|
+
// ../tracker-github/dist/orchestrator-adapter.js
|
|
2129
|
+
import { createHash as createHash3 } from "crypto";
|
|
2130
|
+
var githubProjectTrackerAdapter = {
|
|
2131
|
+
async listIssues(project, dependencies = {}) {
|
|
2132
|
+
return listProjectIssues(project, dependencies);
|
|
2133
|
+
},
|
|
2134
|
+
async listIssuesByStates(project, states, dependencies = {}) {
|
|
2135
|
+
if (states.length === 0) {
|
|
2136
|
+
return [];
|
|
2137
|
+
}
|
|
2138
|
+
const issues = await listProjectIssues(project, dependencies);
|
|
2139
|
+
const normalizedStates = new Set(states.map((state) => state.trim().toLowerCase()));
|
|
2140
|
+
return issues.filter((issue) => normalizedStates.has(issue.state.trim().toLowerCase()));
|
|
2141
|
+
},
|
|
2142
|
+
async fetchIssueStatesByIds(project, issueIds, dependencies = {}) {
|
|
2143
|
+
if (issueIds.length === 0) {
|
|
2144
|
+
return [];
|
|
2145
|
+
}
|
|
2146
|
+
return fetchProjectIssueStatesByIds(project, issueIds, dependencies);
|
|
2147
|
+
},
|
|
2148
|
+
buildWorkerEnvironment(project) {
|
|
2149
|
+
return {
|
|
2150
|
+
GITHUB_PROJECT_ID: requireTrackerSetting(project.tracker, "projectId")
|
|
2151
|
+
};
|
|
2152
|
+
},
|
|
2153
|
+
reviveIssue(project, run) {
|
|
2154
|
+
return {
|
|
2155
|
+
id: run.issueId,
|
|
2156
|
+
identifier: run.issueIdentifier,
|
|
2157
|
+
number: parseIssueNumber(run.issueIdentifier),
|
|
2158
|
+
title: run.issueTitle ?? run.issueIdentifier,
|
|
2159
|
+
description: null,
|
|
2160
|
+
priority: null,
|
|
2161
|
+
state: run.issueState,
|
|
2162
|
+
branchName: null,
|
|
2163
|
+
url: null,
|
|
2164
|
+
labels: [],
|
|
2165
|
+
blockedBy: [],
|
|
2166
|
+
createdAt: null,
|
|
2167
|
+
updatedAt: null,
|
|
2168
|
+
repository: run.repository,
|
|
2169
|
+
tracker: {
|
|
2170
|
+
adapter: "github-project",
|
|
2171
|
+
bindingId: project.tracker.bindingId,
|
|
2172
|
+
itemId: run.issueId
|
|
2173
|
+
},
|
|
2174
|
+
metadata: {}
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
};
|
|
2178
|
+
async function listProjectIssues(project, dependencies = {}) {
|
|
2179
|
+
const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
|
|
2180
|
+
const loadProjectIssues = () => fetchGithubProjectIssues(trackerConfig, dependencies.fetchImpl);
|
|
2181
|
+
return dependencies.projectItemsCache?.getOrLoad(buildProjectItemsCacheKey(trackerConfig, dependencies), loadProjectIssues) ?? loadProjectIssues();
|
|
2182
|
+
}
|
|
2183
|
+
async function fetchProjectIssueStatesByIds(project, issueIds, dependencies = {}) {
|
|
2184
|
+
const trackerConfig = resolveGitHubTrackerConfig(project, dependencies);
|
|
2185
|
+
return fetchGithubIssueStatesByIds(trackerConfig, [...issueIds], dependencies.fetchImpl);
|
|
2186
|
+
}
|
|
2187
|
+
function resolveGitHubTrackerConfig(project, dependencies = {}) {
|
|
2188
|
+
const token = dependencies.token ?? process.env.GITHUB_GRAPHQL_TOKEN;
|
|
2189
|
+
if (!token) {
|
|
2190
|
+
throw new Error("GITHUB_GRAPHQL_TOKEN environment variable is required. Run 'gh auth token' or set the variable.");
|
|
2191
|
+
}
|
|
2192
|
+
const githubProjectId = requireTrackerSetting(project.tracker, "projectId");
|
|
2193
|
+
return {
|
|
2194
|
+
projectId: githubProjectId,
|
|
2195
|
+
token,
|
|
2196
|
+
apiUrl: project.tracker.apiUrl,
|
|
2197
|
+
assignedOnly: readBooleanTrackerSetting(project.tracker, "assignedOnly"),
|
|
2198
|
+
priorityFieldName: readOptionalStringTrackerSetting(project.tracker, "priorityFieldName"),
|
|
2199
|
+
timeoutMs: readNumberTrackerSetting(project.tracker, "timeoutMs")
|
|
2200
|
+
};
|
|
2201
|
+
}
|
|
2202
|
+
function buildProjectItemsCacheKey(config, _dependencies) {
|
|
2203
|
+
return JSON.stringify({
|
|
2204
|
+
adapter: "github-project",
|
|
2205
|
+
apiUrl: config.apiUrl,
|
|
2206
|
+
assignedOnly: config.assignedOnly ?? false,
|
|
2207
|
+
priorityFieldName: config.priorityFieldName ?? null,
|
|
2208
|
+
projectId: config.projectId,
|
|
2209
|
+
timeoutMs: config.timeoutMs,
|
|
2210
|
+
tokenFingerprint: hashToken(config.token)
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
function hashToken(token) {
|
|
2214
|
+
if (!token) {
|
|
2215
|
+
return null;
|
|
2216
|
+
}
|
|
2217
|
+
return createHash3("sha256").update(token).digest("hex");
|
|
2218
|
+
}
|
|
2219
|
+
var trackerAdapters = {
|
|
2220
|
+
"github-project": githubProjectTrackerAdapter
|
|
2221
|
+
};
|
|
2222
|
+
function resolveTrackerAdapter(tracker) {
|
|
2223
|
+
const adapter = trackerAdapters[tracker.adapter];
|
|
2224
|
+
if (!adapter) {
|
|
2225
|
+
throw new Error(`Unsupported tracker adapter: ${tracker.adapter}`);
|
|
2226
|
+
}
|
|
2227
|
+
return adapter;
|
|
2228
|
+
}
|
|
2229
|
+
function requireTrackerSetting(tracker, key) {
|
|
2230
|
+
const value = tracker.settings?.[key];
|
|
2231
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2232
|
+
throw new Error(`Tracker adapter "${tracker.adapter}" requires the "${key}" setting.`);
|
|
2233
|
+
}
|
|
2234
|
+
return value;
|
|
2235
|
+
}
|
|
2236
|
+
function readBooleanTrackerSetting(tracker, key) {
|
|
2237
|
+
const value = tracker.settings?.[key];
|
|
2238
|
+
return value === true || value === "true";
|
|
2239
|
+
}
|
|
2240
|
+
function readNumberTrackerSetting(tracker, key) {
|
|
2241
|
+
const value = tracker.settings?.[key];
|
|
2242
|
+
if (value === void 0) {
|
|
2243
|
+
return void 0;
|
|
2244
|
+
}
|
|
2245
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
2246
|
+
return value;
|
|
2247
|
+
}
|
|
2248
|
+
if (typeof value === "string") {
|
|
2249
|
+
const parsed = Number(value);
|
|
2250
|
+
if (Number.isInteger(parsed) && parsed > 0) {
|
|
2251
|
+
return parsed;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
throw new Error(`Tracker adapter "${tracker.adapter}" requires the "${key}" setting to be a positive integer when provided.`);
|
|
2255
|
+
}
|
|
2256
|
+
function readOptionalStringTrackerSetting(tracker, key) {
|
|
2257
|
+
const value = tracker.settings?.[key];
|
|
2258
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2259
|
+
}
|
|
2260
|
+
function parseIssueNumber(identifier) {
|
|
2261
|
+
const match = identifier.match(/#(\d+)$/);
|
|
2262
|
+
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// ../tracker-file/dist/file-tracker-adapter.js
|
|
2266
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2267
|
+
function requireTrackerSetting2(project, key) {
|
|
2268
|
+
const value = project.tracker.settings?.[key];
|
|
2269
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
2270
|
+
throw new Error(`Tracker adapter "file" requires the "${key}" setting.`);
|
|
2271
|
+
}
|
|
2272
|
+
return value;
|
|
2273
|
+
}
|
|
2274
|
+
function parseIssueNumber2(identifier) {
|
|
2275
|
+
const match = identifier.match(/#(\d+)$/);
|
|
2276
|
+
return match ? Number.parseInt(match[1] ?? "0", 10) : 0;
|
|
2277
|
+
}
|
|
2278
|
+
function isValidIssueShape(entry) {
|
|
2279
|
+
if (!entry || typeof entry !== "object")
|
|
2280
|
+
return false;
|
|
2281
|
+
const e = entry;
|
|
2282
|
+
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";
|
|
2283
|
+
}
|
|
2284
|
+
var fileTrackerAdapter = {
|
|
2285
|
+
async listIssues(project) {
|
|
2286
|
+
const issuesPath = requireTrackerSetting2(project, "issuesPath");
|
|
2287
|
+
try {
|
|
2288
|
+
const raw = await readFile4(issuesPath, "utf-8");
|
|
2289
|
+
const parsed = JSON.parse(raw);
|
|
2290
|
+
if (!Array.isArray(parsed)) {
|
|
2291
|
+
throw new Error(`Expected an array of issues in ${issuesPath}, got ${typeof parsed}`);
|
|
2292
|
+
}
|
|
2293
|
+
const valid = [];
|
|
2294
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
2295
|
+
if (isValidIssueShape(parsed[i])) {
|
|
2296
|
+
valid.push(parsed[i]);
|
|
2297
|
+
} else {
|
|
2298
|
+
process.stderr.write(`[tracker-file] Skipping invalid issue at index ${i} in ${issuesPath}
|
|
2299
|
+
`);
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
return valid;
|
|
2303
|
+
} catch (err) {
|
|
2304
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
2305
|
+
return [];
|
|
2306
|
+
}
|
|
2307
|
+
if (err instanceof SyntaxError) {
|
|
2308
|
+
return [];
|
|
2309
|
+
}
|
|
2310
|
+
throw err;
|
|
2311
|
+
}
|
|
2312
|
+
},
|
|
2313
|
+
async listIssuesByStates(project, states) {
|
|
2314
|
+
if (states.length === 0) {
|
|
2315
|
+
return [];
|
|
2316
|
+
}
|
|
2317
|
+
const issues = await this.listIssues(project);
|
|
2318
|
+
const normalizedStates = new Set(states.map((state) => state.trim().toLowerCase()));
|
|
2319
|
+
return issues.filter((issue) => normalizedStates.has(issue.state.trim().toLowerCase()));
|
|
2320
|
+
},
|
|
2321
|
+
async fetchIssueStatesByIds(project, issueIds) {
|
|
2322
|
+
if (issueIds.length === 0) {
|
|
2323
|
+
return [];
|
|
2324
|
+
}
|
|
2325
|
+
const issues = await this.listIssues(project);
|
|
2326
|
+
const ids = new Set(issueIds);
|
|
2327
|
+
return issues.filter((issue) => ids.has(issue.id));
|
|
2328
|
+
},
|
|
2329
|
+
buildWorkerEnvironment(_project, _issue) {
|
|
2330
|
+
return {
|
|
2331
|
+
SYMPHONY_FILE_TRACKER: "true"
|
|
2332
|
+
};
|
|
2333
|
+
},
|
|
2334
|
+
reviveIssue(project, run) {
|
|
2335
|
+
return {
|
|
2336
|
+
id: run.issueId,
|
|
2337
|
+
identifier: run.issueIdentifier,
|
|
2338
|
+
number: parseIssueNumber2(run.issueIdentifier),
|
|
2339
|
+
title: run.issueTitle ?? run.issueIdentifier,
|
|
2340
|
+
description: null,
|
|
2341
|
+
priority: null,
|
|
2342
|
+
state: run.issueState,
|
|
2343
|
+
branchName: null,
|
|
2344
|
+
url: null,
|
|
2345
|
+
labels: [],
|
|
2346
|
+
blockedBy: [],
|
|
2347
|
+
createdAt: null,
|
|
2348
|
+
updatedAt: null,
|
|
2349
|
+
repository: run.repository,
|
|
2350
|
+
tracker: {
|
|
2351
|
+
adapter: "file",
|
|
2352
|
+
bindingId: project.tracker.bindingId,
|
|
2353
|
+
itemId: run.issueId
|
|
2354
|
+
},
|
|
2355
|
+
metadata: {}
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
};
|
|
2359
|
+
|
|
2360
|
+
// ../orchestrator/dist/tracker-adapters.js
|
|
2361
|
+
var localAdapters = /* @__PURE__ */ new Map([
|
|
2362
|
+
["file", fileTrackerAdapter]
|
|
2363
|
+
]);
|
|
2364
|
+
function resolveTrackerAdapter2(tracker) {
|
|
2365
|
+
const local = localAdapters.get(tracker.adapter);
|
|
2366
|
+
if (local)
|
|
2367
|
+
return local;
|
|
2368
|
+
return resolveTrackerAdapter(tracker);
|
|
2369
|
+
}
|
|
2370
|
+
|
|
2371
|
+
// ../orchestrator/dist/service.js
|
|
2372
|
+
var DEFAULT_POLL_INTERVAL_MS2 = 3e4;
|
|
2373
|
+
var DEFAULT_CONCURRENCY = 3;
|
|
2374
|
+
var DEFAULT_RETRY_BACKOFF_MS = 3e4;
|
|
2375
|
+
var CONTINUATION_RETRY_DELAY_MS = 1e3;
|
|
2376
|
+
var DEFAULT_WORKER_COMMAND = "node packages/worker/dist/index.js";
|
|
2377
|
+
var DEFAULT_MAX_NONPRODUCTIVE_TURNS = 3;
|
|
2378
|
+
var STUCK_WORKER_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
2379
|
+
function isUsableWorkflowResolution(resolution) {
|
|
2380
|
+
return resolution.isValid || resolution.usedLastKnownGood;
|
|
2381
|
+
}
|
|
2382
|
+
function parseTimestampMs(value) {
|
|
2383
|
+
if (!value) {
|
|
2384
|
+
return null;
|
|
2385
|
+
}
|
|
2386
|
+
const parsed = new Date(value).getTime();
|
|
2387
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2388
|
+
}
|
|
2389
|
+
var OrchestratorService = class {
|
|
2390
|
+
store;
|
|
2391
|
+
projectConfig;
|
|
2392
|
+
dependencies;
|
|
2393
|
+
projectPollIntervals = /* @__PURE__ */ new Map();
|
|
2394
|
+
activeWorkerPids = /* @__PURE__ */ new Set();
|
|
2395
|
+
workerStderrBuffers = /* @__PURE__ */ new Map();
|
|
2396
|
+
workerStderrDecoders = /* @__PURE__ */ new Map();
|
|
2397
|
+
lastKnownGoodWorkflows = /* @__PURE__ */ new Map();
|
|
2398
|
+
lastReportedWorkflowErrors = /* @__PURE__ */ new Map();
|
|
2399
|
+
workflowResolutionCache = null;
|
|
2400
|
+
running = true;
|
|
2401
|
+
shuttingDown = false;
|
|
2402
|
+
shutdownPromise = null;
|
|
2403
|
+
sleepTimer = null;
|
|
2404
|
+
sleepResolver = null;
|
|
2405
|
+
reconcilePromise = Promise.resolve();
|
|
2406
|
+
reconcileRequested = false;
|
|
2407
|
+
constructor(store, projectConfig, dependencies = {}) {
|
|
2408
|
+
this.store = store;
|
|
2409
|
+
this.projectConfig = projectConfig;
|
|
2410
|
+
this.dependencies = dependencies;
|
|
2411
|
+
}
|
|
2412
|
+
async run(options = {}) {
|
|
2413
|
+
this.running = true;
|
|
2414
|
+
await this.runSerialized(() => this.performStartupCleanup(this.createTrackerDependencies()));
|
|
2415
|
+
while (this.running) {
|
|
2416
|
+
try {
|
|
2417
|
+
const snapshot = await this.runOnceInternal(options.issueIdentifier, this.createTrackerDependencies());
|
|
2418
|
+
await this.notifyTick(snapshot);
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
if (options.once) {
|
|
2421
|
+
throw error;
|
|
2422
|
+
}
|
|
2423
|
+
this.writeStderr(`[orchestrator] run loop failed for ${this.projectConfig.projectId}: ${this.formatErrorMessage(error)}`);
|
|
2424
|
+
}
|
|
2425
|
+
if (options.once || !this.running) {
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
await this.waitForNextPoll();
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
async runOnce(options = {}) {
|
|
2432
|
+
return this.runOnceInternal(options.issueIdentifier, this.createTrackerDependencies());
|
|
2433
|
+
}
|
|
2434
|
+
async status() {
|
|
2435
|
+
return this.store.loadProjectStatus(this.projectConfig.projectId);
|
|
2436
|
+
}
|
|
2437
|
+
async statusForIssue(issueIdentifier) {
|
|
2438
|
+
const issueRecords = await this.store.loadProjectIssueOrchestrations(this.projectConfig.projectId);
|
|
2439
|
+
const issueRecord = issueRecords.find((record) => record.identifier === issueIdentifier);
|
|
2440
|
+
if (!issueRecord) {
|
|
2441
|
+
return null;
|
|
2442
|
+
}
|
|
2443
|
+
const currentRunCandidate = issueRecord.currentRunId ? await this.store.loadRun(issueRecord.currentRunId, this.projectConfig.projectId) : null;
|
|
2444
|
+
const currentRun = isMatchingIssueRun(currentRunCandidate, this.projectConfig.projectId, issueRecord.issueId, issueIdentifier) ? currentRunCandidate : await this.findLatestRunForIssue(issueRecord.issueId, issueIdentifier);
|
|
2445
|
+
const recentEvents = currentRun === null ? [] : await this.store.loadRecentRunEvents(currentRun.runId, 20, currentRun.projectId);
|
|
2446
|
+
const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
|
|
2447
|
+
const currentAttempt = currentRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
|
|
2448
|
+
return {
|
|
2449
|
+
issue_identifier: issueRecord.identifier,
|
|
2450
|
+
issue_id: issueRecord.issueId,
|
|
2451
|
+
status: currentRun?.status ?? mapIssueOrchestrationStateToStatus(issueRecord.state),
|
|
2452
|
+
workspace: {
|
|
2453
|
+
path: currentRun?.workingDirectory ?? null
|
|
2454
|
+
},
|
|
2455
|
+
attempts: {
|
|
2456
|
+
restart_count: Math.max(0, currentAttempt - 1),
|
|
2457
|
+
current_retry_attempt: currentAttempt
|
|
2458
|
+
},
|
|
2459
|
+
running: currentRun === null ? null : {
|
|
2460
|
+
session_id: currentRun.runtimeSession?.sessionId ?? null,
|
|
2461
|
+
turn_count: currentRun.turnCount ?? null,
|
|
2462
|
+
state: currentRun.issueState ?? null,
|
|
2463
|
+
started_at: currentRun.startedAt ?? null,
|
|
2464
|
+
last_event: currentRun.lastEvent ?? null,
|
|
2465
|
+
last_message: latestEventMessage,
|
|
2466
|
+
last_event_at: currentRun.lastEventAt ?? null,
|
|
2467
|
+
tokens: currentRun.tokenUsage ? {
|
|
2468
|
+
input_tokens: currentRun.tokenUsage.inputTokens,
|
|
2469
|
+
output_tokens: currentRun.tokenUsage.outputTokens,
|
|
2470
|
+
total_tokens: currentRun.tokenUsage.totalTokens
|
|
2471
|
+
} : null
|
|
2472
|
+
},
|
|
2473
|
+
retry: currentRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ? {
|
|
2474
|
+
due_at: currentRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ?? "",
|
|
2475
|
+
kind: currentRun?.retryKind ?? null,
|
|
2476
|
+
error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null
|
|
2477
|
+
} : null,
|
|
2478
|
+
logs: {
|
|
2479
|
+
codex_session_logs: currentRun === null ? [] : [
|
|
2480
|
+
{
|
|
2481
|
+
label: "worker",
|
|
2482
|
+
path: join4(this.store.runDir(currentRun.runId, currentRun.projectId), "worker.log"),
|
|
2483
|
+
url: null
|
|
2484
|
+
}
|
|
2485
|
+
]
|
|
2486
|
+
},
|
|
2487
|
+
recent_events: recentEvents,
|
|
2488
|
+
last_error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null,
|
|
2489
|
+
tracked: {
|
|
2490
|
+
issue_orchestration_state: issueRecord.state,
|
|
2491
|
+
current_run_id: issueRecord.currentRunId,
|
|
2492
|
+
workspace_key: issueRecord.workspaceKey,
|
|
2493
|
+
run_phase: currentRun?.runPhase ?? null,
|
|
2494
|
+
execution_phase: currentRun?.executionPhase ?? null
|
|
2495
|
+
}
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
async recover() {
|
|
2499
|
+
return this.runOnce();
|
|
2500
|
+
}
|
|
2501
|
+
requestReconcile() {
|
|
2502
|
+
this.reconcileRequested = true;
|
|
2503
|
+
this.cancelPendingSleep();
|
|
2504
|
+
}
|
|
2505
|
+
async shutdown() {
|
|
2506
|
+
if (this.shutdownPromise) {
|
|
2507
|
+
return this.shutdownPromise;
|
|
2508
|
+
}
|
|
2509
|
+
this.shuttingDown = true;
|
|
2510
|
+
this.shutdownPromise = (async () => {
|
|
2511
|
+
this.running = false;
|
|
2512
|
+
this.cancelPendingSleep();
|
|
2513
|
+
const workerPids = [...this.activeWorkerPids];
|
|
2514
|
+
for (const pid of workerPids) {
|
|
2515
|
+
this.sendSignal(pid, "SIGTERM");
|
|
2516
|
+
}
|
|
2517
|
+
if (workerPids.length === 0) {
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
let waitedMs = 0;
|
|
2521
|
+
while (this.activeWorkerPids.size > 0 && waitedMs < 1e4) {
|
|
2522
|
+
this.pruneExitedWorkerPids();
|
|
2523
|
+
if (this.activeWorkerPids.size === 0) {
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
await (this.dependencies.waitImpl ?? wait2)(100);
|
|
2527
|
+
waitedMs += 100;
|
|
2528
|
+
}
|
|
2529
|
+
for (const pid of [...this.activeWorkerPids]) {
|
|
2530
|
+
if (!this.isProcessRunning(pid)) {
|
|
2531
|
+
this.retireWorkerPid(pid);
|
|
2532
|
+
continue;
|
|
2533
|
+
}
|
|
2534
|
+
this.sendSignal(pid, "SIGKILL");
|
|
2535
|
+
this.retireWorkerPid(pid);
|
|
2536
|
+
}
|
|
2537
|
+
})();
|
|
2538
|
+
return this.shutdownPromise;
|
|
2539
|
+
}
|
|
2540
|
+
getEffectivePollIntervalMs() {
|
|
2541
|
+
if (this.dependencies.pollIntervalMs) {
|
|
2542
|
+
return this.dependencies.pollIntervalMs;
|
|
2543
|
+
}
|
|
2544
|
+
const configuredIntervals = [...this.projectPollIntervals.values()].filter((value) => Number.isFinite(value) && value > 0);
|
|
2545
|
+
return configuredIntervals.length ? Math.min(...configuredIntervals) : DEFAULT_POLL_INTERVAL_MS2;
|
|
2546
|
+
}
|
|
2547
|
+
async reconcileProject(tenant, issueIdentifier, trackerDependencies = {}) {
|
|
2548
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
2549
|
+
const now = this.now();
|
|
2550
|
+
let lastError = null;
|
|
2551
|
+
let dispatched = 0;
|
|
2552
|
+
let suppressed = 0;
|
|
2553
|
+
let recovered = 0;
|
|
2554
|
+
let pollIntervalMs = DEFAULT_POLL_INTERVAL_MS2;
|
|
2555
|
+
let rateLimits = null;
|
|
2556
|
+
let issueRecords = await this.store.loadProjectIssueOrchestrations(tenant.projectId);
|
|
2557
|
+
const allRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
2558
|
+
const activeRuns = allRuns.filter((run) => isActiveRunStatus(run.status));
|
|
2559
|
+
for (const run of activeRuns) {
|
|
2560
|
+
const outcome = await this.reconcileRun(tenant, run, issueRecords);
|
|
2561
|
+
issueRecords = outcome.issueRecords;
|
|
2562
|
+
if (outcome.recovered) {
|
|
2563
|
+
recovered += 1;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
const reconciledRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId && isActiveRunStatus(run.status));
|
|
2567
|
+
const projectRunsAfterReconcile = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
2568
|
+
rateLimits = resolveProjectRateLimits(reconciledRuns, []);
|
|
2569
|
+
try {
|
|
2570
|
+
pollIntervalMs = await this.loadProjectPollInterval(tenant);
|
|
2571
|
+
const currentActiveRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId && isActiveRunStatus(run.status));
|
|
2572
|
+
const { runs: syncedActiveRuns, issuesByIdentifier: syncedIssuesByIdentifier } = await this.syncActiveRunIssueStates(tenant, trackerAdapter, currentActiveRuns, now);
|
|
2573
|
+
const issues = await trackerAdapter.listIssues(tenant, trackerDependencies);
|
|
2574
|
+
const filteredIssues = issueIdentifier ? issues.filter((issue) => issue.identifier === issueIdentifier) : issues;
|
|
2575
|
+
const { candidates: actionableCandidates, lifecycle } = await this.resolveActionableCandidates(tenant, filteredIssues);
|
|
2576
|
+
const trackedIssuesByIdentifier = new Map(syncedIssuesByIdentifier);
|
|
2577
|
+
for (const issue of filteredIssues) {
|
|
2578
|
+
const existing = trackedIssuesByIdentifier.get(issue.identifier);
|
|
2579
|
+
trackedIssuesByIdentifier.set(issue.identifier, {
|
|
2580
|
+
...existing ?? issue,
|
|
2581
|
+
...issue,
|
|
2582
|
+
rateLimits: issue.rateLimits ?? existing?.rateLimits ?? null
|
|
2583
|
+
});
|
|
2584
|
+
}
|
|
2585
|
+
for (const [identifier, issue] of syncedIssuesByIdentifier) {
|
|
2586
|
+
const existing = trackedIssuesByIdentifier.get(identifier);
|
|
2587
|
+
if (!existing) {
|
|
2588
|
+
trackedIssuesByIdentifier.set(identifier, issue);
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
trackedIssuesByIdentifier.set(identifier, {
|
|
2592
|
+
...issue,
|
|
2593
|
+
...existing,
|
|
2594
|
+
rateLimits: existing.rateLimits ?? issue.rateLimits ?? null
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
rateLimits = resolveProjectRateLimits(syncedActiveRuns, trackedIssuesByIdentifier.values());
|
|
2598
|
+
const concurrency = await this.getProjectConcurrency(tenant);
|
|
2599
|
+
const currentlyActive = issueRecords.filter((record) => isIssueOrchestrationClaimed(record.state)).length;
|
|
2600
|
+
const availableSlots = Math.max(0, concurrency - currentlyActive);
|
|
2601
|
+
const unscheduledCandidates = actionableCandidates.filter((issue) => {
|
|
2602
|
+
if (hasConvergenceLockedRun(projectRunsAfterReconcile, issue.id, issue.state)) {
|
|
2603
|
+
return false;
|
|
2604
|
+
}
|
|
2605
|
+
return !issueRecords.some((record) => record.issueId === issue.id && isIssueOrchestrationClaimed(record.state));
|
|
2606
|
+
});
|
|
2607
|
+
const sortedCandidates = sortCandidatesForDispatch(unscheduledCandidates);
|
|
2608
|
+
const activeByState = /* @__PURE__ */ new Map();
|
|
2609
|
+
for (const run of syncedActiveRuns) {
|
|
2610
|
+
const state = run.issueState;
|
|
2611
|
+
const count = activeByState.get(state) ?? 0;
|
|
2612
|
+
activeByState.set(state, count + 1);
|
|
2613
|
+
}
|
|
2614
|
+
const maxConcurrentByState = await this.loadProjectMaxConcurrentByState(tenant);
|
|
2615
|
+
let slotsRemaining = availableSlots;
|
|
2616
|
+
for (const issue of sortedCandidates) {
|
|
2617
|
+
if (this.shuttingDown) {
|
|
2618
|
+
break;
|
|
2619
|
+
}
|
|
2620
|
+
if (slotsRemaining <= 0)
|
|
2621
|
+
break;
|
|
2622
|
+
if (isIssueBudgetExceeded(resolveIssueBudgetSnapshot(projectRunsAfterReconcile, issue.id), now)) {
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
const stateLimit = maxConcurrentByState[issue.state];
|
|
2626
|
+
if (stateLimit !== void 0) {
|
|
2627
|
+
const activeInState = activeByState.get(issue.state) ?? 0;
|
|
2628
|
+
if (activeInState >= stateLimit) {
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey({
|
|
2633
|
+
projectId: tenant.projectId,
|
|
2634
|
+
adapter: issue.tracker.adapter,
|
|
2635
|
+
issueSubjectId: issue.id
|
|
2636
|
+
}, issue.identifier);
|
|
2637
|
+
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2638
|
+
issueId: issue.id,
|
|
2639
|
+
identifier: issue.identifier,
|
|
2640
|
+
workspaceKey: preferredWorkspaceKey,
|
|
2641
|
+
state: "claimed",
|
|
2642
|
+
currentRunId: null,
|
|
2643
|
+
retryEntry: null,
|
|
2644
|
+
updatedAt: now.toISOString()
|
|
2645
|
+
});
|
|
2646
|
+
let run;
|
|
2647
|
+
try {
|
|
2648
|
+
run = await this.startRun(tenant, issue);
|
|
2649
|
+
} catch (error) {
|
|
2650
|
+
issueRecords = releaseIssueOrchestration(issueRecords, issue.id, now);
|
|
2651
|
+
throw error;
|
|
2652
|
+
}
|
|
2653
|
+
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
2654
|
+
issueId: run.issueId,
|
|
2655
|
+
identifier: run.issueIdentifier,
|
|
2656
|
+
workspaceKey: run.issueWorkspaceKey ?? preferredWorkspaceKey,
|
|
2657
|
+
state: "running",
|
|
2658
|
+
currentRunId: run.runId,
|
|
2659
|
+
retryEntry: null,
|
|
2660
|
+
updatedAt: now.toISOString()
|
|
2661
|
+
});
|
|
2662
|
+
await this.store.saveRun(run);
|
|
2663
|
+
await this.store.appendRunEvent(run.runId, {
|
|
2664
|
+
at: now.toISOString(),
|
|
2665
|
+
event: "run-dispatched",
|
|
2666
|
+
projectId: tenant.projectId,
|
|
2667
|
+
issueIdentifier: issue.identifier,
|
|
2668
|
+
issueId: run.issueId,
|
|
2669
|
+
issueState: issue.state
|
|
2670
|
+
});
|
|
2671
|
+
this.logVerbose(`[dispatch] Issue ${issue.identifier} \u2192 run ${run.runId}`);
|
|
2672
|
+
dispatched += 1;
|
|
2673
|
+
slotsRemaining -= 1;
|
|
2674
|
+
activeByState.set(issue.state, (activeByState.get(issue.state) ?? 0) + 1);
|
|
2675
|
+
}
|
|
2676
|
+
for (const issueRecord of issueRecords) {
|
|
2677
|
+
if (!isIssueOrchestrationClaimed(issueRecord.state)) {
|
|
2678
|
+
continue;
|
|
2679
|
+
}
|
|
2680
|
+
const issue = trackedIssuesByIdentifier.get(issueRecord.identifier);
|
|
2681
|
+
if (!issue) {
|
|
2682
|
+
continue;
|
|
2683
|
+
}
|
|
2684
|
+
const persistedRun = issueRecord.currentRunId ? await this.store.loadRun(issueRecord.currentRunId, tenant.projectId) : null;
|
|
2685
|
+
const activeRun = syncedActiveRuns.find((run) => isMatchingIssueRun(run, tenant.projectId, issueRecord.issueId, issueRecord.identifier)) ?? persistedRun;
|
|
2686
|
+
const resolvedIssue = actionableCandidates.find((candidate) => candidate.identifier === issue.identifier);
|
|
2687
|
+
if (resolvedIssue) {
|
|
2688
|
+
continue;
|
|
2689
|
+
}
|
|
2690
|
+
if (activeRun?.processId) {
|
|
2691
|
+
this.sendSignal(activeRun.processId, "SIGTERM");
|
|
2692
|
+
this.retireWorkerPid(activeRun.processId);
|
|
2693
|
+
}
|
|
2694
|
+
if (activeRun) {
|
|
2695
|
+
const suppressedRun = {
|
|
2696
|
+
...activeRun,
|
|
2697
|
+
status: "suppressed",
|
|
2698
|
+
processId: null,
|
|
2699
|
+
completedAt: now.toISOString(),
|
|
2700
|
+
updatedAt: now.toISOString(),
|
|
2701
|
+
runPhase: "canceled_by_reconciliation",
|
|
2702
|
+
lastError: "Run suppressed because the tracker state is no longer actionable."
|
|
2703
|
+
};
|
|
2704
|
+
await this.store.saveRun(suppressedRun);
|
|
2705
|
+
this.logVerbose(`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`);
|
|
2706
|
+
}
|
|
2707
|
+
issueRecords = releaseIssueOrchestration(issueRecords, issueRecord.issueId, now);
|
|
2708
|
+
suppressed += 1;
|
|
2709
|
+
}
|
|
2710
|
+
const terminalIssuesByIdentifier = /* @__PURE__ */ new Map();
|
|
2711
|
+
for (const issue of trackedIssuesByIdentifier.values()) {
|
|
2712
|
+
if (!isStateTerminal(issue.state, lifecycle)) {
|
|
2713
|
+
continue;
|
|
2714
|
+
}
|
|
2715
|
+
terminalIssuesByIdentifier.set(issue.identifier, issue);
|
|
2716
|
+
}
|
|
2717
|
+
for (const issue of terminalIssuesByIdentifier.values()) {
|
|
2718
|
+
await this.cleanupTerminalIssueWorkspace(tenant, issue, now);
|
|
2719
|
+
}
|
|
2720
|
+
} catch (error) {
|
|
2721
|
+
lastError = error instanceof Error ? error.message : "Unknown orchestration error";
|
|
2722
|
+
}
|
|
2723
|
+
this.projectPollIntervals.set(tenant.projectId, pollIntervalMs);
|
|
2724
|
+
await this.store.saveProjectIssueOrchestrations(tenant.projectId, issueRecords);
|
|
2725
|
+
const allTenantRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
2726
|
+
const latestRuns = allTenantRuns.filter((run) => isActiveRunStatus(run.status));
|
|
2727
|
+
rateLimits = rateLimits ?? resolveProjectRateLimits(latestRuns, []);
|
|
2728
|
+
const status = buildProjectSnapshot({
|
|
2729
|
+
project: tenant,
|
|
2730
|
+
activeRuns: latestRuns,
|
|
2731
|
+
allRuns: allTenantRuns,
|
|
2732
|
+
summary: { dispatched, suppressed, recovered },
|
|
2733
|
+
lastTickAt: now.toISOString(),
|
|
2734
|
+
lastError,
|
|
2735
|
+
rateLimits
|
|
2736
|
+
});
|
|
2737
|
+
await this.store.saveProjectStatus(status);
|
|
2738
|
+
return status;
|
|
2739
|
+
}
|
|
2740
|
+
async performStartupCleanup(trackerDependencies = {}) {
|
|
2741
|
+
const tenant = this.projectConfig;
|
|
2742
|
+
const now = this.now();
|
|
2743
|
+
const workspaceRecords = await this.store.loadIssueWorkspaces(tenant.projectId);
|
|
2744
|
+
if (workspaceRecords.length === 0) {
|
|
2745
|
+
return;
|
|
2746
|
+
}
|
|
2747
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
2748
|
+
const workflowCache = /* @__PURE__ */ new Map();
|
|
2749
|
+
let issues;
|
|
2750
|
+
try {
|
|
2751
|
+
issues = await trackerAdapter.listIssuesByStates(tenant, await this.resolveStartupCleanupTerminalStates(tenant, workspaceRecords, workflowCache), trackerDependencies);
|
|
2752
|
+
} catch (error) {
|
|
2753
|
+
const message = error instanceof Error ? error.message : "Unknown tracker error";
|
|
2754
|
+
console.warn(`[orchestrator] Startup cleanup skipped for project ${tenant.projectId}: ${message}`);
|
|
2755
|
+
return;
|
|
2756
|
+
}
|
|
2757
|
+
const issuesById = new Map(issues.map((issue) => [issue.id, issue]));
|
|
2758
|
+
for (const workspaceRecord of workspaceRecords) {
|
|
2759
|
+
if (workspaceRecord.status === "removed") {
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
const issue = issuesById.get(workspaceRecord.issueSubjectId);
|
|
2763
|
+
if (!issue) {
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
try {
|
|
2767
|
+
const resolution = await this.loadStartupCleanupWorkflow(tenant, issue.repository, workflowCache);
|
|
2768
|
+
if (!resolution.isValid) {
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
if (!isStateTerminal(issue.state, resolution.lifecycle)) {
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
await this.cleanupTerminalIssueWorkspace(tenant, issue, now, resolution);
|
|
2775
|
+
} catch (error) {
|
|
2776
|
+
const message = error instanceof Error ? error.message : "Unknown startup cleanup error";
|
|
2777
|
+
console.warn(`[orchestrator] Startup cleanup skipped workspace for ${issue.identifier}: ${message}`);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
async notifyTick(snapshot) {
|
|
2782
|
+
if (!this.dependencies.onTick) {
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
try {
|
|
2786
|
+
await this.dependencies.onTick(snapshot);
|
|
2787
|
+
} catch (error) {
|
|
2788
|
+
this.writeStderr(`[orchestrator] onTick callback failed: ${this.formatErrorMessage(error)}`);
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
formatErrorMessage(error) {
|
|
2792
|
+
if (error instanceof Error) {
|
|
2793
|
+
return error.stack ?? error.message;
|
|
2794
|
+
}
|
|
2795
|
+
return String(error);
|
|
2796
|
+
}
|
|
2797
|
+
async resolveStartupCleanupTerminalStates(tenant, workspaceRecords, workflowCache) {
|
|
2798
|
+
const terminalStates = /* @__PURE__ */ new Map();
|
|
2799
|
+
const repositories = this.resolveStartupCleanupRepositories(tenant, workspaceRecords);
|
|
2800
|
+
for (const repository of repositories) {
|
|
2801
|
+
let resolution;
|
|
2802
|
+
try {
|
|
2803
|
+
resolution = await this.loadStartupCleanupWorkflow(tenant, repository, workflowCache);
|
|
2804
|
+
} catch {
|
|
2805
|
+
continue;
|
|
2806
|
+
}
|
|
2807
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
for (const state of resolution.lifecycle.terminalStates) {
|
|
2811
|
+
const normalizedState = state.trim().toLowerCase();
|
|
2812
|
+
if (!terminalStates.has(normalizedState)) {
|
|
2813
|
+
terminalStates.set(normalizedState, state);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
if (terminalStates.size === 0) {
|
|
2818
|
+
for (const state of DEFAULT_WORKFLOW_LIFECYCLE.terminalStates) {
|
|
2819
|
+
terminalStates.set(state.trim().toLowerCase(), state);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return [...terminalStates.values()];
|
|
2823
|
+
}
|
|
2824
|
+
resolveStartupCleanupRepositories(tenant, workspaceRecords) {
|
|
2825
|
+
const repositories = /* @__PURE__ */ new Map();
|
|
2826
|
+
for (const repository of tenant.repositories) {
|
|
2827
|
+
repositories.set(this.startupCleanupRepositoryKey(repository.owner, repository.name), repository);
|
|
2828
|
+
}
|
|
2829
|
+
for (const workspaceRecord of workspaceRecords) {
|
|
2830
|
+
const repository = this.parseWorkspaceRepositoryRef(workspaceRecord);
|
|
2831
|
+
if (!repository) {
|
|
2832
|
+
continue;
|
|
2833
|
+
}
|
|
2834
|
+
const key = this.startupCleanupRepositoryKey(repository.owner, repository.name);
|
|
2835
|
+
if (!repositories.has(key)) {
|
|
2836
|
+
repositories.set(key, repository);
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
return [...repositories.values()];
|
|
2840
|
+
}
|
|
2841
|
+
parseWorkspaceRepositoryRef(workspaceRecord) {
|
|
2842
|
+
const match = workspaceRecord.issueIdentifier.match(/^([^/]+)\/([^#]+)#\d+$/);
|
|
2843
|
+
if (!match) {
|
|
2844
|
+
return null;
|
|
2845
|
+
}
|
|
2846
|
+
const owner = match[1];
|
|
2847
|
+
const name = match[2];
|
|
2848
|
+
if (!owner || !name) {
|
|
2849
|
+
return null;
|
|
2850
|
+
}
|
|
2851
|
+
return {
|
|
2852
|
+
owner,
|
|
2853
|
+
name,
|
|
2854
|
+
cloneUrl: workspaceRecord.repositoryPath
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
startupCleanupRepositoryKey(owner, name) {
|
|
2858
|
+
return `${owner}/${name}`;
|
|
2859
|
+
}
|
|
2860
|
+
async loadStartupCleanupWorkflow(tenant, repository, workflowCache) {
|
|
2861
|
+
const cacheKey = this.workflowCacheKey(repository);
|
|
2862
|
+
const cachedResolution = workflowCache.get(cacheKey);
|
|
2863
|
+
if (cachedResolution) {
|
|
2864
|
+
return cachedResolution;
|
|
2865
|
+
}
|
|
2866
|
+
const resolutionPromise = tenant.repositories.some((candidate) => candidate.owner === repository.owner && candidate.name === repository.name) ? this.loadProjectWorkflow(tenant, repository) : loadRepositoryWorkflow(repository.cloneUrl, repository);
|
|
2867
|
+
workflowCache.set(cacheKey, resolutionPromise);
|
|
2868
|
+
return resolutionPromise;
|
|
2869
|
+
}
|
|
2870
|
+
async runSerialized(operation) {
|
|
2871
|
+
const previous = this.reconcilePromise;
|
|
2872
|
+
let release;
|
|
2873
|
+
this.reconcilePromise = new Promise((resolve6) => {
|
|
2874
|
+
release = resolve6;
|
|
2875
|
+
});
|
|
2876
|
+
await previous;
|
|
2877
|
+
try {
|
|
2878
|
+
return await operation();
|
|
2879
|
+
} finally {
|
|
2880
|
+
release();
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
async runOnceInternal(issueIdentifier, trackerDependencies) {
|
|
2884
|
+
return this.runSerialized(async () => {
|
|
2885
|
+
const workflowResolutionCache = /* @__PURE__ */ new Map();
|
|
2886
|
+
this.workflowResolutionCache = workflowResolutionCache;
|
|
2887
|
+
try {
|
|
2888
|
+
return await this.reconcileProject(this.projectConfig, issueIdentifier, trackerDependencies);
|
|
2889
|
+
} finally {
|
|
2890
|
+
if (this.workflowResolutionCache === workflowResolutionCache) {
|
|
2891
|
+
this.workflowResolutionCache = null;
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
createTrackerDependencies() {
|
|
2897
|
+
return {
|
|
2898
|
+
fetchImpl: this.dependencies.fetchImpl,
|
|
2899
|
+
projectItemsCache: createProjectItemsCache()
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
async findLatestRunForIssue(issueId, issueIdentifier) {
|
|
2903
|
+
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());
|
|
2904
|
+
return matchingRuns[0] ?? null;
|
|
2905
|
+
}
|
|
2906
|
+
async resolveActionableCandidates(tenant, issues) {
|
|
2907
|
+
const candidates = [];
|
|
2908
|
+
let lifecycle = null;
|
|
2909
|
+
for (const issue of issues) {
|
|
2910
|
+
const resolution = await this.loadProjectWorkflow(tenant, issue.repository);
|
|
2911
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
2912
|
+
continue;
|
|
2913
|
+
}
|
|
2914
|
+
if (!lifecycle) {
|
|
2915
|
+
lifecycle = resolution.lifecycle;
|
|
2916
|
+
}
|
|
2917
|
+
if (!isStateActive(issue.state, resolution.lifecycle)) {
|
|
2918
|
+
continue;
|
|
2919
|
+
}
|
|
2920
|
+
if (matchesWorkflowState(issue.state, resolution.lifecycle.blockerCheckStates) && issue.blockedBy.length > 0) {
|
|
2921
|
+
const hasNonTerminalBlocker = issue.blockedBy.some((blockerRef) => {
|
|
2922
|
+
if (blockerRef.state && isStateTerminal(blockerRef.state, resolution.lifecycle)) {
|
|
2923
|
+
return false;
|
|
2924
|
+
}
|
|
2925
|
+
if (blockerRef.identifier) {
|
|
2926
|
+
const blockerIssue = issues.find((candidate) => candidate.identifier === blockerRef.identifier);
|
|
2927
|
+
if (blockerIssue?.state) {
|
|
2928
|
+
return !isStateTerminal(blockerIssue.state, resolution.lifecycle);
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
return true;
|
|
2932
|
+
});
|
|
2933
|
+
if (hasNonTerminalBlocker) {
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
candidates.push(issue);
|
|
2938
|
+
}
|
|
2939
|
+
if (!lifecycle && tenant.repositories.length > 0) {
|
|
2940
|
+
const resolution = await this.loadProjectWorkflow(tenant, tenant.repositories[0]);
|
|
2941
|
+
if (isUsableWorkflowResolution(resolution)) {
|
|
2942
|
+
lifecycle = resolution.lifecycle;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
return {
|
|
2946
|
+
candidates,
|
|
2947
|
+
lifecycle: lifecycle ?? {
|
|
2948
|
+
stateFieldName: "Status",
|
|
2949
|
+
activeStates: ["Todo", "In Progress"],
|
|
2950
|
+
terminalStates: ["Done"],
|
|
2951
|
+
blockerCheckStates: ["Todo"]
|
|
2952
|
+
}
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
async loadProjectWorkflow(tenant, repository) {
|
|
2956
|
+
const cacheKey = this.workflowCacheKey(repository);
|
|
2957
|
+
const pendingCache = this.workflowResolutionCache;
|
|
2958
|
+
if (pendingCache) {
|
|
2959
|
+
const cachedResolution = pendingCache.get(cacheKey);
|
|
2960
|
+
if (cachedResolution) {
|
|
2961
|
+
return cachedResolution;
|
|
2962
|
+
}
|
|
2963
|
+
const resolutionPromise = this.loadProjectWorkflowUncached(tenant, repository);
|
|
2964
|
+
pendingCache.set(cacheKey, resolutionPromise);
|
|
2965
|
+
return resolutionPromise;
|
|
2966
|
+
}
|
|
2967
|
+
return this.loadProjectWorkflowUncached(tenant, repository);
|
|
2968
|
+
}
|
|
2969
|
+
async loadProjectWorkflowUncached(tenant, repository) {
|
|
2970
|
+
const cacheRoot = join4(this.store.projectDir(tenant.projectId), "cache", repository.owner, repository.name);
|
|
2971
|
+
const { repositoryDirectory, changed } = await syncRepositoryForRun({
|
|
2972
|
+
repository,
|
|
2973
|
+
targetDirectory: cacheRoot
|
|
2974
|
+
});
|
|
2975
|
+
const resolution = await loadRepositoryWorkflow(repositoryDirectory, repository);
|
|
2976
|
+
return this.resolveWorkflowResolution(repository, cacheRoot, resolution, changed);
|
|
2977
|
+
}
|
|
2978
|
+
async startRun(tenant, issue, resumeContext) {
|
|
2979
|
+
if (this.shuttingDown || !this.running) {
|
|
2980
|
+
throw new Error("Orchestrator is shutting down and cannot start new runs.");
|
|
2981
|
+
}
|
|
2982
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
2983
|
+
const now = this.now();
|
|
2984
|
+
const runId = createRunId(now, tenant.projectId, issue.identifier);
|
|
2985
|
+
const runDir = this.store.runDir(runId, tenant.projectId);
|
|
2986
|
+
const workspaceRuntimeDir = runDir;
|
|
2987
|
+
const issueSubjectId = issue.id;
|
|
2988
|
+
const identity = {
|
|
2989
|
+
projectId: tenant.projectId,
|
|
2990
|
+
adapter: issue.tracker.adapter,
|
|
2991
|
+
issueSubjectId
|
|
2992
|
+
};
|
|
2993
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(identity, issue.identifier);
|
|
2994
|
+
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(identity);
|
|
2995
|
+
const existingWorkspaceRecord = await this.store.loadIssueWorkspace(tenant.projectId, preferredWorkspaceKey) ?? (legacyWorkspaceKey === preferredWorkspaceKey ? null : await this.store.loadIssueWorkspace(tenant.projectId, legacyWorkspaceKey));
|
|
2996
|
+
const workspaceKey = existingWorkspaceRecord?.workspaceKey ?? preferredWorkspaceKey;
|
|
2997
|
+
const projectDir = this.store.projectDir(tenant.projectId);
|
|
2998
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(projectDir, workspaceKey);
|
|
2999
|
+
const repositoryDirectory = await ensureIssueWorkspaceRepository({
|
|
3000
|
+
repository: issue.repository,
|
|
3001
|
+
issueWorkspacePath
|
|
3002
|
+
});
|
|
3003
|
+
if (!existingWorkspaceRecord) {
|
|
3004
|
+
const workspaceRecord = {
|
|
3005
|
+
workspaceKey,
|
|
3006
|
+
projectId: tenant.projectId,
|
|
3007
|
+
adapter: issue.tracker.adapter,
|
|
3008
|
+
issueSubjectId,
|
|
3009
|
+
issueIdentifier: issue.identifier,
|
|
3010
|
+
workspacePath: issueWorkspacePath,
|
|
3011
|
+
repositoryPath: repositoryDirectory,
|
|
3012
|
+
status: "active",
|
|
3013
|
+
createdAt: now.toISOString(),
|
|
3014
|
+
updatedAt: now.toISOString(),
|
|
3015
|
+
lastError: null
|
|
3016
|
+
};
|
|
3017
|
+
await this.store.saveIssueWorkspace(workspaceRecord);
|
|
3018
|
+
const afterCreateResult = await this.runHook("after_create", tenant, repositoryDirectory, issue.repository, {
|
|
3019
|
+
projectId: tenant.projectId,
|
|
3020
|
+
workspaceKey,
|
|
3021
|
+
issueSubjectId,
|
|
3022
|
+
issueIdentifier: issue.identifier,
|
|
3023
|
+
workspacePath: issueWorkspacePath,
|
|
3024
|
+
repositoryPath: repositoryDirectory
|
|
3025
|
+
});
|
|
3026
|
+
if (afterCreateResult && afterCreateResult.outcome !== "success" && afterCreateResult.outcome !== "skipped") {
|
|
3027
|
+
await this.store.appendRunEvent(runId, {
|
|
3028
|
+
at: now.toISOString(),
|
|
3029
|
+
event: "hook-failed",
|
|
3030
|
+
projectId: tenant.projectId,
|
|
3031
|
+
hook: "after_create",
|
|
3032
|
+
error: afterCreateResult.error ?? null
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
}
|
|
3036
|
+
const workflow = await this.loadProjectWorkflow(tenant, issue.repository);
|
|
3037
|
+
if (!isUsableWorkflowResolution(workflow)) {
|
|
3038
|
+
throw new Error(workflow.validationError ?? "Invalid repository WORKFLOW.md");
|
|
3039
|
+
}
|
|
3040
|
+
const allProjectRuns = (await this.store.loadAllRuns()).filter((run) => run.projectId === tenant.projectId);
|
|
3041
|
+
const issueBudgetSnapshot = resolveIssueBudgetSnapshot(allProjectRuns, issue.id);
|
|
3042
|
+
const promptVariables = buildPromptVariables(issue, {
|
|
3043
|
+
attempt: null
|
|
3044
|
+
// first execution
|
|
3045
|
+
});
|
|
3046
|
+
const renderedPrompt = renderPrompt(workflow.promptTemplate, promptVariables);
|
|
3047
|
+
await this.runHook("before_run", tenant, repositoryDirectory, issue.repository, {
|
|
3048
|
+
projectId: tenant.projectId,
|
|
3049
|
+
workspaceKey,
|
|
3050
|
+
issueSubjectId,
|
|
3051
|
+
issueIdentifier: issue.identifier,
|
|
3052
|
+
workspacePath: issueWorkspacePath,
|
|
3053
|
+
repositoryPath: repositoryDirectory,
|
|
3054
|
+
runId,
|
|
3055
|
+
state: issue.state
|
|
3056
|
+
});
|
|
3057
|
+
mkdirSync(runDir, { recursive: true });
|
|
3058
|
+
const workerLogStream = (this.dependencies.createWriteStreamImpl ?? createWriteStream)(join4(runDir, "worker.log"), {
|
|
3059
|
+
flags: "a"
|
|
3060
|
+
});
|
|
3061
|
+
let workerLogAvailable = true;
|
|
3062
|
+
let workerExited = false;
|
|
3063
|
+
let workerStderrFinalizing = false;
|
|
3064
|
+
let workerLogBackpressured = false;
|
|
3065
|
+
const resumeWorkerStderr = () => {
|
|
3066
|
+
if (!workerLogBackpressured) {
|
|
3067
|
+
return;
|
|
3068
|
+
}
|
|
3069
|
+
workerLogBackpressured = false;
|
|
3070
|
+
child.stderr?.resume?.();
|
|
3071
|
+
};
|
|
3072
|
+
const markWorkerLogUnavailable = (error) => {
|
|
3073
|
+
resumeWorkerStderr();
|
|
3074
|
+
if (!workerLogAvailable) {
|
|
3075
|
+
return;
|
|
3076
|
+
}
|
|
3077
|
+
workerLogAvailable = false;
|
|
3078
|
+
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
3079
|
+
this.writeStderr(`[orchestrator] failed to write worker log for ${runId}: ${message}`);
|
|
3080
|
+
};
|
|
3081
|
+
const child = (this.dependencies.spawnImpl ?? spawn3)("bash", ["-lc", resolveWorkerCommand()], {
|
|
3082
|
+
cwd: process.cwd(),
|
|
3083
|
+
env: this.buildProjectExecutionEnv(tenant.projectId, {
|
|
3084
|
+
GITHUB_GRAPHQL_TOKEN: process.env.GITHUB_GRAPHQL_TOKEN ?? "",
|
|
3085
|
+
CODEX_PROJECT_ID: tenant.projectId,
|
|
3086
|
+
PROJECT_ID: tenant.projectId,
|
|
3087
|
+
WORKING_DIRECTORY: repositoryDirectory,
|
|
3088
|
+
WORKSPACE_RUNTIME_DIR: workspaceRuntimeDir,
|
|
3089
|
+
SYMPHONY_RUN_ID: runId,
|
|
3090
|
+
SYMPHONY_ISSUE_STATE: issue.state,
|
|
3091
|
+
SYMPHONY_ISSUE_ID: issue.id,
|
|
3092
|
+
SYMPHONY_ISSUE_IDENTIFIER: issue.identifier,
|
|
3093
|
+
SYMPHONY_ISSUE_TITLE: issue.title,
|
|
3094
|
+
SYMPHONY_ISSUE_SUBJECT_ID: issueSubjectId,
|
|
3095
|
+
SYMPHONY_ISSUE_WORKSPACE_KEY: workspaceKey,
|
|
3096
|
+
SYMPHONY_TRACKER_ADAPTER: issue.tracker.adapter,
|
|
3097
|
+
SYMPHONY_TRACKER_BINDING_ID: issue.tracker.bindingId,
|
|
3098
|
+
SYMPHONY_TRACKER_ITEM_ID: issue.tracker.itemId,
|
|
3099
|
+
TARGET_REPOSITORY_CLONE_URL: issue.repository.cloneUrl,
|
|
3100
|
+
TARGET_REPOSITORY_OWNER: issue.repository.owner,
|
|
3101
|
+
TARGET_REPOSITORY_NAME: issue.repository.name,
|
|
3102
|
+
TARGET_REPOSITORY_URL: issue.repository.url,
|
|
3103
|
+
...trackerAdapter.buildWorkerEnvironment(tenant, issue),
|
|
3104
|
+
SYMPHONY_RENDERED_PROMPT: renderedPrompt,
|
|
3105
|
+
SYMPHONY_WORKFLOW_PATH: workflow.workflowPath ?? "",
|
|
3106
|
+
SYMPHONY_AGENT_COMMAND: workflow.workflow.codex.command,
|
|
3107
|
+
SYMPHONY_APPROVAL_POLICY: workflow.workflow.codex.approvalPolicy ?? "",
|
|
3108
|
+
SYMPHONY_THREAD_SANDBOX: workflow.workflow.codex.threadSandbox ?? "",
|
|
3109
|
+
SYMPHONY_TURN_SANDBOX_POLICY: workflow.workflow.codex.turnSandboxPolicy ?? "",
|
|
3110
|
+
SYMPHONY_MAX_TURNS: String(workflow.workflow.agent.maxTurns),
|
|
3111
|
+
SYMPHONY_GLOBAL_MAX_TURNS: process.env.SYMPHONY_GLOBAL_MAX_TURNS ?? "",
|
|
3112
|
+
SYMPHONY_MAX_TOKENS: process.env.SYMPHONY_MAX_TOKENS ?? "",
|
|
3113
|
+
SYMPHONY_MAX_NONPRODUCTIVE_TURNS: process.env.SYMPHONY_MAX_NONPRODUCTIVE_TURNS ?? String(DEFAULT_MAX_NONPRODUCTIVE_TURNS),
|
|
3114
|
+
SYMPHONY_SESSION_TIMEOUT_MS: process.env.SYMPHONY_SESSION_TIMEOUT_MS ?? "",
|
|
3115
|
+
SYMPHONY_RESUME_THREAD_ID: resumeContext?.threadId ?? "",
|
|
3116
|
+
SYMPHONY_CUMULATIVE_TURN_COUNT: String(Math.max(0, resumeContext?.cumulativeTurnCount ?? issueBudgetSnapshot.cumulativeTurnCount)),
|
|
3117
|
+
SYMPHONY_CUMULATIVE_INPUT_TOKENS: String(issueBudgetSnapshot.tokenUsage.inputTokens),
|
|
3118
|
+
SYMPHONY_CUMULATIVE_OUTPUT_TOKENS: String(issueBudgetSnapshot.tokenUsage.outputTokens),
|
|
3119
|
+
SYMPHONY_CUMULATIVE_TOTAL_TOKENS: String(issueBudgetSnapshot.tokenUsage.totalTokens),
|
|
3120
|
+
SYMPHONY_LAST_TURN_SUMMARY: resumeContext?.lastTurnSummary ?? "",
|
|
3121
|
+
SYMPHONY_SESSION_STARTED_AT: issueBudgetSnapshot.sessionStartedAt ?? "",
|
|
3122
|
+
SYMPHONY_READ_TIMEOUT_MS: String(workflow.workflow.codex.readTimeoutMs),
|
|
3123
|
+
SYMPHONY_TURN_TIMEOUT_MS: String(workflow.workflow.codex.turnTimeoutMs)
|
|
3124
|
+
}),
|
|
3125
|
+
detached: true,
|
|
3126
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
3127
|
+
});
|
|
3128
|
+
const handleWorkerStderrChunk = (chunk) => {
|
|
3129
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
|
|
3130
|
+
if (workerLogAvailable) {
|
|
3131
|
+
try {
|
|
3132
|
+
if (!workerLogStream.write(buffer)) {
|
|
3133
|
+
workerLogBackpressured = true;
|
|
3134
|
+
child.stderr?.pause?.();
|
|
3135
|
+
}
|
|
3136
|
+
} catch (error) {
|
|
3137
|
+
markWorkerLogUnavailable(error);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
this.consumeWorkerStderrChunk(runId, buffer);
|
|
3141
|
+
};
|
|
3142
|
+
const drainWorkerStderr = () => {
|
|
3143
|
+
const stderr = child.stderr;
|
|
3144
|
+
if (!stderr || typeof stderr.read !== "function") {
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
let chunk;
|
|
3148
|
+
while ((chunk = stderr.read()) !== null) {
|
|
3149
|
+
handleWorkerStderrChunk(chunk);
|
|
3150
|
+
}
|
|
3151
|
+
};
|
|
3152
|
+
const completeWorkerStderrFinalization = (code, signal) => {
|
|
3153
|
+
if (workerExited) {
|
|
3154
|
+
return;
|
|
3155
|
+
}
|
|
3156
|
+
workerExited = true;
|
|
3157
|
+
workerStderrFinalizing = false;
|
|
3158
|
+
child.stderr?.removeListener("data", handleWorkerStderrChunk);
|
|
3159
|
+
this.flushWorkerStderrBuffer(runId);
|
|
3160
|
+
workerLogStream.end();
|
|
3161
|
+
if (child.pid) {
|
|
3162
|
+
this.retireWorkerPid(child.pid);
|
|
3163
|
+
}
|
|
3164
|
+
this.logVerbose(`[worker-exited] ${runId} (code=${code ?? "null"}, signal=${signal ?? "null"})`);
|
|
3165
|
+
};
|
|
3166
|
+
const finalizeWorkerStderr = (code, signal) => {
|
|
3167
|
+
if (workerExited || workerStderrFinalizing) {
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
workerStderrFinalizing = true;
|
|
3171
|
+
const stderr = child.stderr;
|
|
3172
|
+
const finish = () => {
|
|
3173
|
+
stderr?.removeListener("end", finish);
|
|
3174
|
+
stderr?.removeListener("close", finish);
|
|
3175
|
+
drainWorkerStderr();
|
|
3176
|
+
completeWorkerStderrFinalization(code, signal);
|
|
3177
|
+
};
|
|
3178
|
+
resumeWorkerStderr();
|
|
3179
|
+
drainWorkerStderr();
|
|
3180
|
+
if (!stderr) {
|
|
3181
|
+
completeWorkerStderrFinalization(code, signal);
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
if (stderr.readableEnded || stderr.readable === false) {
|
|
3185
|
+
finish();
|
|
3186
|
+
return;
|
|
3187
|
+
}
|
|
3188
|
+
stderr.once("end", finish);
|
|
3189
|
+
stderr.once("close", finish);
|
|
3190
|
+
};
|
|
3191
|
+
workerLogStream.on("error", (error) => {
|
|
3192
|
+
markWorkerLogUnavailable(error);
|
|
3193
|
+
});
|
|
3194
|
+
workerLogStream.on("drain", () => {
|
|
3195
|
+
resumeWorkerStderr();
|
|
3196
|
+
});
|
|
3197
|
+
child.stderr?.on("data", handleWorkerStderrChunk);
|
|
3198
|
+
if (child.pid) {
|
|
3199
|
+
this.activeWorkerPids.add(child.pid);
|
|
3200
|
+
this.logVerbose(`[worker-started] ${runId} (pid=${child.pid})`);
|
|
3201
|
+
}
|
|
3202
|
+
child.on?.("error", (error) => {
|
|
3203
|
+
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
3204
|
+
this.writeStderr(`[orchestrator] worker process error for ${runId}: ${message}`);
|
|
3205
|
+
finalizeWorkerStderr(null, null);
|
|
3206
|
+
});
|
|
3207
|
+
child.on?.("close", (code, signal) => {
|
|
3208
|
+
finalizeWorkerStderr(code, signal);
|
|
3209
|
+
});
|
|
3210
|
+
child.unref();
|
|
3211
|
+
return {
|
|
3212
|
+
runId,
|
|
3213
|
+
projectId: tenant.projectId,
|
|
3214
|
+
projectSlug: tenant.slug,
|
|
3215
|
+
issueId: issue.id,
|
|
3216
|
+
issueSubjectId,
|
|
3217
|
+
issueIdentifier: issue.identifier,
|
|
3218
|
+
issueTitle: issue.title,
|
|
3219
|
+
issueState: issue.state,
|
|
3220
|
+
repository: issue.repository,
|
|
3221
|
+
status: "running",
|
|
3222
|
+
attempt: 1,
|
|
3223
|
+
processId: child.pid ?? null,
|
|
3224
|
+
port: null,
|
|
3225
|
+
workingDirectory: repositoryDirectory,
|
|
3226
|
+
issueWorkspaceKey: workspaceKey,
|
|
3227
|
+
workspaceRuntimeDir,
|
|
3228
|
+
workflowPath: workflow.workflowPath,
|
|
3229
|
+
retryKind: null,
|
|
3230
|
+
threadId: null,
|
|
3231
|
+
cumulativeTurnCount: 0,
|
|
3232
|
+
lastTurnSummary: null,
|
|
3233
|
+
createdAt: now.toISOString(),
|
|
3234
|
+
updatedAt: now.toISOString(),
|
|
3235
|
+
startedAt: now.toISOString(),
|
|
3236
|
+
completedAt: null,
|
|
3237
|
+
lastError: null,
|
|
3238
|
+
nextRetryAt: null,
|
|
3239
|
+
runPhase: "preparing_workspace",
|
|
3240
|
+
rateLimits: issue.rateLimits ?? null
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
async syncActiveRunIssueStates(tenant, trackerAdapter, activeRuns, now) {
|
|
3244
|
+
const activeIssueIds = [...new Set(activeRuns.map((run) => run.issueId))];
|
|
3245
|
+
if (activeIssueIds.length === 0) {
|
|
3246
|
+
return {
|
|
3247
|
+
runs: activeRuns,
|
|
3248
|
+
issuesByIdentifier: /* @__PURE__ */ new Map()
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
const issues = await trackerAdapter.fetchIssueStatesByIds(tenant, activeIssueIds, {
|
|
3252
|
+
fetchImpl: this.dependencies.fetchImpl
|
|
3253
|
+
});
|
|
3254
|
+
const issuesByIdentifier = new Map(issues.map((issue) => [issue.identifier, issue]));
|
|
3255
|
+
const issueStateByIdentifier = new Map(issues.map((issue) => [issue.identifier, issue.state]));
|
|
3256
|
+
const syncedRuns = [];
|
|
3257
|
+
for (const run of activeRuns) {
|
|
3258
|
+
const currentTrackerState = issueStateByIdentifier.get(run.issueIdentifier);
|
|
3259
|
+
if (!currentTrackerState || currentTrackerState === run.issueState) {
|
|
3260
|
+
syncedRuns.push(run);
|
|
3261
|
+
continue;
|
|
3262
|
+
}
|
|
3263
|
+
const updatedRun = {
|
|
3264
|
+
...run,
|
|
3265
|
+
issueState: currentTrackerState,
|
|
3266
|
+
updatedAt: now.toISOString()
|
|
3267
|
+
};
|
|
3268
|
+
await this.store.saveRun(updatedRun);
|
|
3269
|
+
syncedRuns.push(updatedRun);
|
|
3270
|
+
}
|
|
3271
|
+
return {
|
|
3272
|
+
runs: syncedRuns,
|
|
3273
|
+
issuesByIdentifier
|
|
3274
|
+
};
|
|
3275
|
+
}
|
|
3276
|
+
async reconcileRun(tenant, run, issueRecords) {
|
|
3277
|
+
const now = this.now();
|
|
3278
|
+
if (run.processId && this.isProcessRunning(run.processId)) {
|
|
3279
|
+
const retryPolicy = await this.loadRetryPolicy(tenant, run.repository);
|
|
3280
|
+
const configuredStallTimeoutMs = retryPolicy?.stallTimeoutMs ?? null;
|
|
3281
|
+
const lastActivityAtMs = parseTimestampMs(run.lastEventAt ?? run.startedAt);
|
|
3282
|
+
const startedAtMs = parseTimestampMs(run.startedAt);
|
|
3283
|
+
const elapsedSinceLastActivityMs = lastActivityAtMs === null ? null : now.getTime() - lastActivityAtMs;
|
|
3284
|
+
const runningSinceMs = startedAtMs === null ? null : now.getTime() - startedAtMs;
|
|
3285
|
+
const isStalledByWorkflowTimeout = configuredStallTimeoutMs !== null && configuredStallTimeoutMs > 0 && elapsedSinceLastActivityMs !== null && elapsedSinceLastActivityMs > configuredStallTimeoutMs;
|
|
3286
|
+
const isStalledByFallbackTimeout = runningSinceMs !== null && runningSinceMs > STUCK_WORKER_TIMEOUT_MS;
|
|
3287
|
+
if (isStalledByWorkflowTimeout || isStalledByFallbackTimeout) {
|
|
3288
|
+
const elapsedMs = isStalledByWorkflowTimeout ? elapsedSinceLastActivityMs : runningSinceMs;
|
|
3289
|
+
const timeoutMs = isStalledByWorkflowTimeout ? configuredStallTimeoutMs : STUCK_WORKER_TIMEOUT_MS;
|
|
3290
|
+
const elapsedSeconds = Math.round((elapsedMs ?? 0) / 1e3);
|
|
3291
|
+
const timeoutSeconds = Math.round((timeoutMs ?? 0) / 1e3);
|
|
3292
|
+
if (this.isVerboseLoggingEnabled()) {
|
|
3293
|
+
this.writeStderr(`[stall-detected] ${run.runId} (elapsed=${elapsedSeconds}s > ${timeoutSeconds}s)`);
|
|
3294
|
+
} else {
|
|
3295
|
+
this.writeStderr(`[orchestrator] stuck worker detected for ${run.runId} (elapsed ${elapsedSeconds}s > ${timeoutSeconds}s) \u2014 sending SIGTERM`);
|
|
3296
|
+
}
|
|
3297
|
+
this.sendSignal(run.processId, "SIGTERM");
|
|
3298
|
+
} else {
|
|
3299
|
+
const runningRecord = {
|
|
3300
|
+
...run,
|
|
3301
|
+
status: "running",
|
|
3302
|
+
updatedAt: now.toISOString()
|
|
3303
|
+
};
|
|
3304
|
+
await this.store.saveRun(runningRecord);
|
|
3305
|
+
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
3306
|
+
issueId: run.issueId,
|
|
3307
|
+
identifier: run.issueIdentifier,
|
|
3308
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey({
|
|
3309
|
+
projectId: tenant.projectId,
|
|
3310
|
+
adapter: tenant.tracker.adapter,
|
|
3311
|
+
issueSubjectId: run.issueSubjectId
|
|
3312
|
+
}, run.issueIdentifier),
|
|
3313
|
+
state: "running",
|
|
3314
|
+
currentRunId: run.runId,
|
|
3315
|
+
retryEntry: null,
|
|
3316
|
+
updatedAt: now.toISOString()
|
|
3317
|
+
});
|
|
3318
|
+
return {
|
|
3319
|
+
issueRecords,
|
|
3320
|
+
recovered: false
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
if (run.processId) {
|
|
3325
|
+
this.retireWorkerPid(run.processId);
|
|
3326
|
+
}
|
|
3327
|
+
const workerInfo = await this.fetchWorkerRunInfo(run);
|
|
3328
|
+
const runWithTokens = {
|
|
3329
|
+
...run,
|
|
3330
|
+
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),
|
|
3331
|
+
threadId: workerInfo.threadId ?? run.threadId ?? null,
|
|
3332
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(run, workerInfo.turnCount ?? null),
|
|
3333
|
+
tokenUsage: workerInfo.tokenUsage ?? run.tokenUsage,
|
|
3334
|
+
lastEvent: workerInfo.lastEvent ?? run.lastEvent,
|
|
3335
|
+
lastTurnSummary: resolveLastTurnSummary(run.lastTurnSummary, resolveLastTurnSummaryCandidate(workerInfo.lastEvent, workerInfo.lastError)),
|
|
3336
|
+
lastEventAt: workerInfo.lastEventAt ?? run.lastEventAt ?? void 0,
|
|
3337
|
+
lastEventAtSource: workerInfo.lastEventAtSource ?? run.lastEventAtSource ?? void 0,
|
|
3338
|
+
executionPhase: workerInfo.executionPhase ?? run.executionPhase ?? null,
|
|
3339
|
+
runPhase: workerInfo.runPhase ?? run.runPhase ?? null,
|
|
3340
|
+
rateLimits: workerInfo.rateLimits ?? run.rateLimits ?? null
|
|
3341
|
+
};
|
|
3342
|
+
const workerSessionId = workerInfo.sessionId;
|
|
3343
|
+
if (workerInfo.lastError) {
|
|
3344
|
+
await this.store.appendRunEvent(run.runId, {
|
|
3345
|
+
at: now.toISOString(),
|
|
3346
|
+
event: "worker-error",
|
|
3347
|
+
projectId: run.projectId,
|
|
3348
|
+
runId: run.runId,
|
|
3349
|
+
issueIdentifier: run.issueIdentifier,
|
|
3350
|
+
error: workerInfo.lastError,
|
|
3351
|
+
attempt: run.attempt
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
if (run.status === "retrying" && run.nextRetryAt) {
|
|
3355
|
+
if (new Date(run.nextRetryAt).getTime() > now.getTime()) {
|
|
3356
|
+
return {
|
|
3357
|
+
issueRecords,
|
|
3358
|
+
recovered: false
|
|
3359
|
+
};
|
|
3360
|
+
}
|
|
3361
|
+
if (await this.resolveRetryRestartAction(tenant, run) === "release") {
|
|
3362
|
+
return this.releaseRetryingRun(runWithTokens, issueRecords, now);
|
|
3363
|
+
}
|
|
3364
|
+
return this.restartRun(tenant, run, issueRecords, now, workerSessionId);
|
|
3365
|
+
}
|
|
3366
|
+
if (workerInfo.exitClassification === "budget-exceeded" || workerInfo.exitClassification === "convergence-detected") {
|
|
3367
|
+
const completedRun = {
|
|
3368
|
+
...runWithTokens,
|
|
3369
|
+
status: workerInfo.exitClassification === "budget-exceeded" ? "succeeded" : "failed",
|
|
3370
|
+
processId: null,
|
|
3371
|
+
updatedAt: now.toISOString(),
|
|
3372
|
+
completedAt: now.toISOString(),
|
|
3373
|
+
nextRetryAt: null,
|
|
3374
|
+
retryKind: null,
|
|
3375
|
+
lastError: workerInfo.exitClassification === "budget-exceeded" ? null : runWithTokens.lastError,
|
|
3376
|
+
runPhase: runWithTokens.runPhase ?? (workerInfo.exitClassification === "budget-exceeded" ? "succeeded" : "failed")
|
|
3377
|
+
};
|
|
3378
|
+
await this.store.saveRun(completedRun);
|
|
3379
|
+
this.logVerbose(`[run-completed] ${completedRun.runId} status=${completedRun.status}`);
|
|
3380
|
+
return {
|
|
3381
|
+
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
3382
|
+
recovered: false
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
if (run.issueWorkspaceKey) {
|
|
3386
|
+
const issueWorkspacePath = resolveIssueWorkspaceDirectory(this.store.projectDir(tenant.projectId), run.issueWorkspaceKey);
|
|
3387
|
+
await this.runHook("after_run", tenant, run.workingDirectory, run.repository, {
|
|
3388
|
+
projectId: run.projectId,
|
|
3389
|
+
workspaceKey: run.issueWorkspaceKey,
|
|
3390
|
+
issueSubjectId: run.issueSubjectId,
|
|
3391
|
+
issueIdentifier: run.issueIdentifier,
|
|
3392
|
+
workspacePath: issueWorkspacePath,
|
|
3393
|
+
repositoryPath: run.workingDirectory,
|
|
3394
|
+
runId: run.runId,
|
|
3395
|
+
state: run.issueState
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
const retryKind = await this.classifyRetryKind(tenant, run);
|
|
3399
|
+
let nextRetryAt;
|
|
3400
|
+
if (retryKind === "continuation") {
|
|
3401
|
+
nextRetryAt = new Date(now.getTime() + CONTINUATION_RETRY_DELAY_MS).toISOString();
|
|
3402
|
+
} else {
|
|
3403
|
+
const retryOptions = await this.loadRetryPolicy(tenant, run.repository);
|
|
3404
|
+
const backoffMs = this.dependencies.retryBackoffMs ?? DEFAULT_RETRY_BACKOFF_MS;
|
|
3405
|
+
nextRetryAt = (retryOptions ? scheduleRetryAt(now, run.attempt + 1, retryOptions) : new Date(now.getTime() + backoffMs)).toISOString();
|
|
3406
|
+
}
|
|
3407
|
+
const retryRecord = {
|
|
3408
|
+
...runWithTokens,
|
|
3409
|
+
status: "retrying",
|
|
3410
|
+
attempt: runWithTokens.attempt + 1,
|
|
3411
|
+
processId: null,
|
|
3412
|
+
updatedAt: now.toISOString(),
|
|
3413
|
+
nextRetryAt,
|
|
3414
|
+
retryKind,
|
|
3415
|
+
threadId: runWithTokens.threadId ?? runWithTokens.runtimeSession?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
3416
|
+
cumulativeTurnCount: runWithTokens.cumulativeTurnCount ?? run.cumulativeTurnCount ?? 0,
|
|
3417
|
+
lastTurnSummary: runWithTokens.lastTurnSummary ?? run.lastTurnSummary ?? null,
|
|
3418
|
+
runPhase: runWithTokens.runPhase ?? "failed",
|
|
3419
|
+
lastError: retryKind === "continuation" ? null : "Worker process exited unexpectedly."
|
|
3420
|
+
};
|
|
3421
|
+
await this.store.saveRun(retryRecord);
|
|
3422
|
+
this.logVerbose(`[retry-scheduled] ${retryRecord.runId} kind=${retryKind} attempt=${retryRecord.attempt} nextAt=${nextRetryAt}`);
|
|
3423
|
+
this.logVerbose(`[run-completed] ${retryRecord.runId} status=${retryRecord.status}`);
|
|
3424
|
+
issueRecords = upsertIssueOrchestration(issueRecords, {
|
|
3425
|
+
issueId: run.issueId,
|
|
3426
|
+
identifier: run.issueIdentifier,
|
|
3427
|
+
workspaceKey: run.issueWorkspaceKey ?? deriveIssueWorkspaceKey({
|
|
3428
|
+
projectId: tenant.projectId,
|
|
3429
|
+
adapter: tenant.tracker.adapter,
|
|
3430
|
+
issueSubjectId: run.issueSubjectId
|
|
3431
|
+
}, run.issueIdentifier),
|
|
3432
|
+
state: "retry_queued",
|
|
3433
|
+
completedOnce: retryKind === "continuation" ? true : void 0,
|
|
3434
|
+
currentRunId: run.runId,
|
|
3435
|
+
retryEntry: {
|
|
3436
|
+
attempt: retryRecord.attempt,
|
|
3437
|
+
dueAt: nextRetryAt,
|
|
3438
|
+
error: retryRecord.lastError
|
|
3439
|
+
},
|
|
3440
|
+
updatedAt: now.toISOString()
|
|
3441
|
+
});
|
|
3442
|
+
return {
|
|
3443
|
+
issueRecords,
|
|
3444
|
+
recovered: false
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
now() {
|
|
3448
|
+
return this.dependencies.now?.() ?? /* @__PURE__ */ new Date();
|
|
3449
|
+
}
|
|
3450
|
+
isVerboseLoggingEnabled() {
|
|
3451
|
+
return this.dependencies.logLevel === "verbose";
|
|
3452
|
+
}
|
|
3453
|
+
writeStderr(message) {
|
|
3454
|
+
(this.dependencies.stderr ?? process.stderr).write(`${message}
|
|
3455
|
+
`);
|
|
3456
|
+
}
|
|
3457
|
+
consumeWorkerStderrChunk(runId, chunk) {
|
|
3458
|
+
let decoder = this.workerStderrDecoders.get(runId);
|
|
3459
|
+
if (!decoder) {
|
|
3460
|
+
decoder = new StringDecoder("utf8");
|
|
3461
|
+
this.workerStderrDecoders.set(runId, decoder);
|
|
3462
|
+
}
|
|
3463
|
+
const nextBuffer = (this.workerStderrBuffers.get(runId) ?? "") + decoder.write(chunk);
|
|
3464
|
+
const lines = nextBuffer.split("\n");
|
|
3465
|
+
this.workerStderrBuffers.set(runId, lines.pop() ?? "");
|
|
3466
|
+
for (const line of lines) {
|
|
3467
|
+
this.consumeWorkerStderrLine(runId, line);
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
flushWorkerStderrBuffer(runId) {
|
|
3471
|
+
const decoder = this.workerStderrDecoders.get(runId);
|
|
3472
|
+
const remainder = (this.workerStderrBuffers.get(runId) ?? "") + (decoder?.end() ?? "");
|
|
3473
|
+
this.workerStderrBuffers.delete(runId);
|
|
3474
|
+
this.workerStderrDecoders.delete(runId);
|
|
3475
|
+
if (remainder && remainder.trim()) {
|
|
3476
|
+
this.consumeWorkerStderrLine(runId, remainder);
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
consumeWorkerStderrLine(runId, line) {
|
|
3480
|
+
const trimmed = line.trim();
|
|
3481
|
+
if (!trimmed || !trimmed.startsWith("{")) {
|
|
3482
|
+
return;
|
|
3483
|
+
}
|
|
3484
|
+
try {
|
|
3485
|
+
const parsed = JSON.parse(trimmed);
|
|
3486
|
+
if (!isOrchestratorChannelEvent(parsed)) {
|
|
3487
|
+
return;
|
|
3488
|
+
}
|
|
3489
|
+
void this.runSerialized(() => this.applyWorkerChannelEvent(runId, parsed)).catch((error) => {
|
|
3490
|
+
const message = error instanceof Error ? error.message : String(error ?? "unknown");
|
|
3491
|
+
this.writeStderr(`[orchestrator] failed to apply worker channel event for ${runId}: ${message}`);
|
|
3492
|
+
});
|
|
3493
|
+
} catch {
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
async applyWorkerChannelEvent(runId, event) {
|
|
3497
|
+
const run = await this.store.loadRun(runId, this.projectConfig.projectId);
|
|
3498
|
+
if (!run || !canApplyWorkerChannelUpdate(run.status) || run.issueId !== event.issueId) {
|
|
3499
|
+
return;
|
|
3500
|
+
}
|
|
3501
|
+
if (event.type === "heartbeat") {
|
|
3502
|
+
const nowIso2 = this.now().toISOString();
|
|
3503
|
+
const persistedLastEventAt = event.lastEventAt ?? run.lastEventAt ?? null;
|
|
3504
|
+
await this.store.saveRun({
|
|
3505
|
+
...run,
|
|
3506
|
+
updatedAt: nowIso2,
|
|
3507
|
+
lastEvent: "heartbeat",
|
|
3508
|
+
lastTurnSummary: resolveLastTurnSummary(run.lastTurnSummary, event.lastError),
|
|
3509
|
+
lastEventAt: persistedLastEventAt,
|
|
3510
|
+
lastEventAtSource: event.lastEventAt != null ? "event-channel" : run.lastEventAtSource ?? null,
|
|
3511
|
+
tokenUsage: event.tokenUsage,
|
|
3512
|
+
rateLimits: event.rateLimits,
|
|
3513
|
+
runtimeSession: buildRuntimeSession(run.runtimeSession, resolveChannelSessionId(event.sessionInfo), event.sessionInfo?.threadId ?? null, "active", run.startedAt ?? run.runtimeSession?.startedAt ?? nowIso2, nowIso2, event.sessionInfo?.exitClassification ?? null),
|
|
3514
|
+
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
3515
|
+
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
3516
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(run, event.sessionInfo?.turnCount ?? null),
|
|
3517
|
+
executionPhase: event.executionPhase ?? run.executionPhase,
|
|
3518
|
+
runPhase: event.runPhase ?? run.runPhase,
|
|
3519
|
+
lastError: event.lastError
|
|
3520
|
+
});
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
3523
|
+
if (event.type === "turn_started") {
|
|
3524
|
+
await this.store.appendRunEvent(runId, {
|
|
3525
|
+
at: event.startedAt,
|
|
3526
|
+
event: "turn_started",
|
|
3527
|
+
projectId: run.projectId,
|
|
3528
|
+
issueIdentifier: run.issueIdentifier,
|
|
3529
|
+
issueId: run.issueId,
|
|
3530
|
+
sessionId: event.sessionId,
|
|
3531
|
+
threadId: event.threadId,
|
|
3532
|
+
turnId: event.turnId,
|
|
3533
|
+
turnCount: event.turnCount
|
|
3534
|
+
});
|
|
3535
|
+
return;
|
|
3536
|
+
}
|
|
3537
|
+
if (event.type === "turn_completed") {
|
|
3538
|
+
await this.store.appendRunEvent(runId, {
|
|
3539
|
+
at: event.completedAt,
|
|
3540
|
+
event: "turn_completed",
|
|
3541
|
+
projectId: run.projectId,
|
|
3542
|
+
issueIdentifier: run.issueIdentifier,
|
|
3543
|
+
issueId: run.issueId,
|
|
3544
|
+
sessionId: event.sessionId,
|
|
3545
|
+
threadId: event.threadId,
|
|
3546
|
+
turnId: event.turnId,
|
|
3547
|
+
turnCount: event.turnCount,
|
|
3548
|
+
startedAt: event.startedAt,
|
|
3549
|
+
durationMs: event.durationMs,
|
|
3550
|
+
tokenUsage: event.tokenUsage
|
|
3551
|
+
});
|
|
3552
|
+
return;
|
|
3553
|
+
}
|
|
3554
|
+
if (event.type === "turn_failed") {
|
|
3555
|
+
await this.store.appendRunEvent(runId, {
|
|
3556
|
+
at: event.failedAt,
|
|
3557
|
+
event: "turn_failed",
|
|
3558
|
+
projectId: run.projectId,
|
|
3559
|
+
issueIdentifier: run.issueIdentifier,
|
|
3560
|
+
issueId: run.issueId,
|
|
3561
|
+
sessionId: event.sessionId,
|
|
3562
|
+
threadId: event.threadId,
|
|
3563
|
+
turnId: event.turnId,
|
|
3564
|
+
turnCount: event.turnCount,
|
|
3565
|
+
startedAt: event.startedAt,
|
|
3566
|
+
durationMs: event.durationMs,
|
|
3567
|
+
tokenUsage: event.tokenUsage,
|
|
3568
|
+
error: event.error
|
|
3569
|
+
});
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
const nowIso = this.now().toISOString();
|
|
3573
|
+
await this.store.saveRun({
|
|
3574
|
+
...run,
|
|
3575
|
+
updatedAt: nowIso,
|
|
3576
|
+
lastEvent: event.event ?? run.lastEvent ?? null,
|
|
3577
|
+
lastTurnSummary: resolveLastTurnSummary(run.lastTurnSummary, resolveLastTurnSummaryCandidate(event.event, event.lastError)),
|
|
3578
|
+
lastEventAt: event.lastEventAt,
|
|
3579
|
+
lastEventAtSource: "event-channel",
|
|
3580
|
+
tokenUsage: event.tokenUsage ?? run.tokenUsage,
|
|
3581
|
+
rateLimits: event.rateLimits ?? run.rateLimits ?? null,
|
|
3582
|
+
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),
|
|
3583
|
+
threadId: event.sessionInfo?.threadId ?? run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
3584
|
+
turnCount: event.sessionInfo && event.sessionInfo.turnCount != null ? event.sessionInfo.turnCount : run.turnCount,
|
|
3585
|
+
cumulativeTurnCount: resolveCumulativeTurnCount(run, event.sessionInfo?.turnCount ?? null),
|
|
3586
|
+
executionPhase: event.executionPhase ?? run.executionPhase ?? null,
|
|
3587
|
+
runPhase: event.runPhase ?? run.runPhase ?? null,
|
|
3588
|
+
lastError: event.lastError ?? run.lastError
|
|
3589
|
+
});
|
|
3590
|
+
}
|
|
3591
|
+
logVerbose(message) {
|
|
3592
|
+
if (!this.isVerboseLoggingEnabled()) {
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
this.writeStderr(message);
|
|
3596
|
+
}
|
|
3597
|
+
async waitForNextPoll() {
|
|
3598
|
+
if (this.consumePendingReconcileRequest()) {
|
|
3599
|
+
return;
|
|
3600
|
+
}
|
|
3601
|
+
const customWait = this.dependencies.waitImpl;
|
|
3602
|
+
const pollIntervalMs = this.getEffectivePollIntervalMs();
|
|
3603
|
+
const waitPromise = this.createPendingSleepPromise();
|
|
3604
|
+
try {
|
|
3605
|
+
if (customWait) {
|
|
3606
|
+
await Promise.race([customWait(pollIntervalMs), waitPromise]);
|
|
3607
|
+
} else {
|
|
3608
|
+
this.sleepTimer = setTimeout(() => {
|
|
3609
|
+
this.sleepResolver?.();
|
|
3610
|
+
}, pollIntervalMs);
|
|
3611
|
+
await waitPromise;
|
|
3612
|
+
}
|
|
3613
|
+
} finally {
|
|
3614
|
+
this.cancelPendingSleep();
|
|
3615
|
+
}
|
|
3616
|
+
this.consumePendingReconcileRequest();
|
|
3617
|
+
}
|
|
3618
|
+
cancelPendingSleep() {
|
|
3619
|
+
if (this.sleepTimer) {
|
|
3620
|
+
clearTimeout(this.sleepTimer);
|
|
3621
|
+
this.sleepTimer = null;
|
|
3622
|
+
}
|
|
3623
|
+
this.sleepResolver?.();
|
|
3624
|
+
this.sleepResolver = null;
|
|
3625
|
+
}
|
|
3626
|
+
createPendingSleepPromise() {
|
|
3627
|
+
return new Promise((resolve6) => {
|
|
3628
|
+
this.sleepResolver = () => {
|
|
3629
|
+
this.sleepResolver = null;
|
|
3630
|
+
this.sleepTimer = null;
|
|
3631
|
+
resolve6();
|
|
3632
|
+
};
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
consumePendingReconcileRequest() {
|
|
3636
|
+
if (!this.reconcileRequested) {
|
|
3637
|
+
return false;
|
|
3638
|
+
}
|
|
3639
|
+
this.reconcileRequested = false;
|
|
3640
|
+
return true;
|
|
3641
|
+
}
|
|
3642
|
+
/**
|
|
3643
|
+
* Classify whether a process exit should be treated as continuation retry
|
|
3644
|
+
* or failure retry. Continuation applies when the issue is still actionable
|
|
3645
|
+
* — the worker completed its session and the issue hasn't transitioned away.
|
|
3646
|
+
* Failure applies when we cannot confirm the issue is still actionable.
|
|
3647
|
+
*/
|
|
3648
|
+
async classifyRetryKind(tenant, run) {
|
|
3649
|
+
try {
|
|
3650
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
3651
|
+
const issues = await trackerAdapter.listIssues(tenant, {
|
|
3652
|
+
fetchImpl: this.dependencies.fetchImpl
|
|
3653
|
+
});
|
|
3654
|
+
const runIssue = issues.find((issue) => issue.identifier === run.issueIdentifier);
|
|
3655
|
+
if (!runIssue) {
|
|
3656
|
+
return "failure";
|
|
3657
|
+
}
|
|
3658
|
+
const resolution = await this.loadProjectWorkflow(tenant, run.repository);
|
|
3659
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
3660
|
+
return "failure";
|
|
3661
|
+
}
|
|
3662
|
+
return isStateActive(runIssue.state, resolution.lifecycle) ? "continuation" : "failure";
|
|
3663
|
+
} catch {
|
|
3664
|
+
return "failure";
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
async resolveRetryRestartAction(tenant, run) {
|
|
3668
|
+
try {
|
|
3669
|
+
if (isIssueBudgetExceeded(resolveIssueBudgetSnapshot((await this.store.loadAllRuns()).filter((candidate) => candidate.projectId === tenant.projectId), run.issueId), this.now())) {
|
|
3670
|
+
return "release";
|
|
3671
|
+
}
|
|
3672
|
+
const runIssue = await this.fetchTrackedIssueById(tenant, run.issueId);
|
|
3673
|
+
if (!runIssue) {
|
|
3674
|
+
return "release";
|
|
3675
|
+
}
|
|
3676
|
+
const resolution = await this.loadProjectWorkflow(tenant, run.repository);
|
|
3677
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
3678
|
+
return "restart";
|
|
3679
|
+
}
|
|
3680
|
+
return isStateActive(runIssue.state, resolution.lifecycle) ? "restart" : "release";
|
|
3681
|
+
} catch {
|
|
3682
|
+
return "restart";
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
async fetchTrackedIssueById(tenant, issueId) {
|
|
3686
|
+
const trackerAdapter = resolveTrackerAdapter2(tenant.tracker);
|
|
3687
|
+
const issues = await trackerAdapter.fetchIssueStatesByIds(tenant, [issueId], {
|
|
3688
|
+
fetchImpl: this.dependencies.fetchImpl
|
|
3689
|
+
});
|
|
3690
|
+
return issues[0] ?? null;
|
|
3691
|
+
}
|
|
3692
|
+
async fetchWorkerRunInfo(run) {
|
|
3693
|
+
const latestRun = await this.store.loadRun(run.runId, run.projectId) ?? run;
|
|
3694
|
+
const persistedTokenUsage = await this.readPersistedWorkerTokenUsage(latestRun);
|
|
3695
|
+
return {
|
|
3696
|
+
tokenUsage: persistedTokenUsage,
|
|
3697
|
+
sessionId: latestRun.runtimeSession?.sessionId ?? null,
|
|
3698
|
+
threadId: latestRun.threadId ?? latestRun.runtimeSession?.threadId ?? null,
|
|
3699
|
+
turnCount: latestRun.turnCount ?? null,
|
|
3700
|
+
exitClassification: latestRun.runtimeSession?.exitClassification ?? null,
|
|
3701
|
+
lastError: latestRun.lastError ?? null,
|
|
3702
|
+
lastEvent: latestRun.lastEvent ?? null,
|
|
3703
|
+
lastEventAt: latestRun.lastEventAt ?? null,
|
|
3704
|
+
lastEventAtSource: latestRun.lastEventAtSource ?? null,
|
|
3705
|
+
executionPhase: latestRun.executionPhase ?? null,
|
|
3706
|
+
runPhase: latestRun.runPhase ?? null,
|
|
3707
|
+
rateLimits: latestRun.rateLimits ?? null
|
|
3708
|
+
};
|
|
3709
|
+
}
|
|
3710
|
+
async readPersistedWorkerTokenUsage(run) {
|
|
3711
|
+
const artifactPaths = [
|
|
3712
|
+
join4(run.workspaceRuntimeDir, "token-usage.json"),
|
|
3713
|
+
join4(run.workspaceRuntimeDir, ".orchestrator", "runs", run.runId, "token-usage.json")
|
|
3714
|
+
];
|
|
3715
|
+
for (const artifactPath of artifactPaths) {
|
|
3716
|
+
try {
|
|
3717
|
+
const raw = await readFile5(artifactPath, "utf8");
|
|
3718
|
+
const tokenUsage = JSON.parse(raw);
|
|
3719
|
+
if (hasTokenUsage(tokenUsage)) {
|
|
3720
|
+
return tokenUsage;
|
|
3721
|
+
}
|
|
3722
|
+
} catch {
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
return null;
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* Execute a workspace lifecycle hook using the workflow configuration
|
|
3730
|
+
* loaded from the repository. Returns the hook result or null if the
|
|
3731
|
+
* workflow could not be loaded.
|
|
3732
|
+
*/
|
|
3733
|
+
async runHook(kind, tenant, repositoryDirectory, repository, context, resolution) {
|
|
3734
|
+
try {
|
|
3735
|
+
const workflowResolution = resolution ?? await this.loadProjectWorkflow(tenant, repository);
|
|
3736
|
+
if (!isUsableWorkflowResolution(workflowResolution)) {
|
|
3737
|
+
return null;
|
|
3738
|
+
}
|
|
3739
|
+
const hookEnv = this.buildProjectExecutionEnv(tenant.projectId, buildHookEnv(context));
|
|
3740
|
+
return executeWorkspaceHook({
|
|
3741
|
+
kind,
|
|
3742
|
+
hooks: workflowResolution.workflow.hooks,
|
|
3743
|
+
repositoryPath: repositoryDirectory,
|
|
3744
|
+
env: hookEnv,
|
|
3745
|
+
timeoutMs: workflowResolution.workflow.hooks.timeoutMs
|
|
3746
|
+
});
|
|
3747
|
+
} catch {
|
|
3748
|
+
return null;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
readProjectEnv(projectId) {
|
|
3752
|
+
const envPath = join4(this.store.projectDir(projectId), ".env");
|
|
3753
|
+
try {
|
|
3754
|
+
return readEnvFile(envPath);
|
|
3755
|
+
} catch (error) {
|
|
3756
|
+
const message = error instanceof Error ? error.message : "Unknown error occurred.";
|
|
3757
|
+
(this.dependencies.stderr ?? process.stderr).write(`[warn] Failed to load project env for ${projectId} from ${envPath}: ${message}
|
|
3758
|
+
`);
|
|
3759
|
+
return {};
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
buildProjectExecutionEnv(projectId, env) {
|
|
3763
|
+
const inheritedEnv = Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string"));
|
|
3764
|
+
const explicitEnv = Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
|
|
3765
|
+
return {
|
|
3766
|
+
...this.readProjectEnv(projectId),
|
|
3767
|
+
...inheritedEnv,
|
|
3768
|
+
...explicitEnv
|
|
3769
|
+
};
|
|
3770
|
+
}
|
|
3771
|
+
async restartRun(tenant, run, issueRecords, now, sessionId) {
|
|
3772
|
+
const supersededRecord = {
|
|
3773
|
+
...run,
|
|
3774
|
+
status: "failed",
|
|
3775
|
+
completedAt: now.toISOString(),
|
|
3776
|
+
updatedAt: now.toISOString(),
|
|
3777
|
+
lastError: "Superseded by recovered run."
|
|
3778
|
+
};
|
|
3779
|
+
await this.store.saveRun(supersededRecord);
|
|
3780
|
+
const issue = resolveTrackerAdapter2(tenant.tracker).reviveIssue(tenant, run);
|
|
3781
|
+
const restarted = await this.startRun(tenant, issue, {
|
|
3782
|
+
threadId: run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
3783
|
+
cumulativeTurnCount: resolvePersistedCumulativeTurnCount(run),
|
|
3784
|
+
lastTurnSummary: run.lastTurnSummary ?? null
|
|
3785
|
+
});
|
|
3786
|
+
const recoveredRecord = {
|
|
3787
|
+
...restarted,
|
|
3788
|
+
attempt: run.attempt,
|
|
3789
|
+
retryKind: run.retryKind ?? "recovery",
|
|
3790
|
+
createdAt: run.createdAt,
|
|
3791
|
+
issueWorkspaceKey: run.issueWorkspaceKey,
|
|
3792
|
+
threadId: run.threadId ?? run.runtimeSession?.threadId ?? null,
|
|
3793
|
+
cumulativeTurnCount: resolvePersistedCumulativeTurnCount(run),
|
|
3794
|
+
lastTurnSummary: run.lastTurnSummary ?? null,
|
|
3795
|
+
turnCount: 0
|
|
3796
|
+
};
|
|
3797
|
+
await this.store.saveRun(recoveredRecord);
|
|
3798
|
+
await this.store.appendRunEvent(run.runId, {
|
|
3799
|
+
at: now.toISOString(),
|
|
3800
|
+
event: "run-recovered",
|
|
3801
|
+
projectId: run.projectId,
|
|
3802
|
+
issueIdentifier: run.issueIdentifier,
|
|
3803
|
+
issueId: run.issueId,
|
|
3804
|
+
sessionId: sessionId ?? void 0
|
|
3805
|
+
});
|
|
3806
|
+
return {
|
|
3807
|
+
issueRecords: upsertIssueOrchestration(issueRecords, {
|
|
3808
|
+
issueId: recoveredRecord.issueId,
|
|
3809
|
+
identifier: recoveredRecord.issueIdentifier,
|
|
3810
|
+
workspaceKey: recoveredRecord.issueWorkspaceKey ?? deriveIssueWorkspaceKey({
|
|
3811
|
+
projectId: tenant.projectId,
|
|
3812
|
+
adapter: tenant.tracker.adapter,
|
|
3813
|
+
issueSubjectId: recoveredRecord.issueSubjectId
|
|
3814
|
+
}, recoveredRecord.issueIdentifier),
|
|
3815
|
+
state: "running",
|
|
3816
|
+
currentRunId: recoveredRecord.runId,
|
|
3817
|
+
retryEntry: null,
|
|
3818
|
+
updatedAt: now.toISOString()
|
|
3819
|
+
}),
|
|
3820
|
+
recovered: true
|
|
3821
|
+
};
|
|
3822
|
+
}
|
|
3823
|
+
async releaseRetryingRun(run, issueRecords, now) {
|
|
3824
|
+
const suppressedRun = {
|
|
3825
|
+
...run,
|
|
3826
|
+
status: "suppressed",
|
|
3827
|
+
processId: null,
|
|
3828
|
+
completedAt: now.toISOString(),
|
|
3829
|
+
updatedAt: now.toISOString(),
|
|
3830
|
+
nextRetryAt: null,
|
|
3831
|
+
runPhase: "canceled_by_reconciliation",
|
|
3832
|
+
lastError: "Retry canceled because the tracker issue is no longer actionable."
|
|
3833
|
+
};
|
|
3834
|
+
await this.store.saveRun(suppressedRun);
|
|
3835
|
+
this.logVerbose(`[run-completed] ${suppressedRun.runId} status=${suppressedRun.status}`);
|
|
3836
|
+
return {
|
|
3837
|
+
issueRecords: releaseIssueOrchestration(issueRecords, run.issueId, now),
|
|
3838
|
+
recovered: false
|
|
3839
|
+
};
|
|
3840
|
+
}
|
|
3841
|
+
async loadProjectPollInterval(tenant) {
|
|
3842
|
+
const intervals = await Promise.all(tenant.repositories.map(async (repository) => {
|
|
3843
|
+
const resolution = await this.loadProjectWorkflow(tenant, repository);
|
|
3844
|
+
return isUsableWorkflowResolution(resolution) ? resolution.workflow.polling.intervalMs : NaN;
|
|
3845
|
+
}));
|
|
3846
|
+
const validIntervals = intervals.filter((value) => Number.isFinite(value) && value > 0);
|
|
3847
|
+
return validIntervals.length ? Math.min(...validIntervals) : DEFAULT_POLL_INTERVAL_MS2;
|
|
3848
|
+
}
|
|
3849
|
+
async loadProjectMaxConcurrentByState(tenant) {
|
|
3850
|
+
const result = {};
|
|
3851
|
+
const resolutions = await Promise.all(tenant.repositories.map(async (repository) => {
|
|
3852
|
+
try {
|
|
3853
|
+
return await this.loadProjectWorkflow(tenant, repository);
|
|
3854
|
+
} catch {
|
|
3855
|
+
return null;
|
|
3856
|
+
}
|
|
3857
|
+
}));
|
|
3858
|
+
for (const resolution of resolutions) {
|
|
3859
|
+
if (!resolution)
|
|
3860
|
+
continue;
|
|
3861
|
+
if (!isUsableWorkflowResolution(resolution))
|
|
3862
|
+
continue;
|
|
3863
|
+
const stateLimits = resolution.workflow.agent.maxConcurrentAgentsByState;
|
|
3864
|
+
for (const [state, limit] of Object.entries(stateLimits)) {
|
|
3865
|
+
const existing = result[state];
|
|
3866
|
+
const numLimit = typeof limit === "number" ? limit : Number(limit);
|
|
3867
|
+
result[state] = existing === void 0 ? numLimit : Math.min(existing, numLimit);
|
|
3868
|
+
}
|
|
3869
|
+
}
|
|
3870
|
+
return result;
|
|
3871
|
+
}
|
|
3872
|
+
async loadRetryPolicy(tenant, repository) {
|
|
3873
|
+
try {
|
|
3874
|
+
const resolution = await this.loadProjectWorkflow(tenant, repository);
|
|
3875
|
+
if (!isUsableWorkflowResolution(resolution)) {
|
|
3876
|
+
return null;
|
|
3877
|
+
}
|
|
3878
|
+
return {
|
|
3879
|
+
baseDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.retryBaseDelayMs,
|
|
3880
|
+
maxDelayMs: this.dependencies.retryBackoffMs ?? resolution.workflow.agent.maxRetryBackoffMs,
|
|
3881
|
+
stallTimeoutMs: resolution.workflow.codex.stallTimeoutMs
|
|
3882
|
+
};
|
|
3883
|
+
} catch {
|
|
3884
|
+
if (!this.dependencies.retryBackoffMs) {
|
|
3885
|
+
return null;
|
|
3886
|
+
}
|
|
3887
|
+
return {
|
|
3888
|
+
baseDelayMs: this.dependencies.retryBackoffMs,
|
|
3889
|
+
maxDelayMs: this.dependencies.retryBackoffMs,
|
|
3890
|
+
stallTimeoutMs: null
|
|
3891
|
+
};
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
async getProjectConcurrency(project) {
|
|
3895
|
+
if (this.dependencies.concurrency !== void 0) {
|
|
3896
|
+
return this.dependencies.concurrency;
|
|
3897
|
+
}
|
|
3898
|
+
const limits = await Promise.all(project.repositories.map(async (repository) => {
|
|
3899
|
+
try {
|
|
3900
|
+
const resolution = await this.loadProjectWorkflow(project, repository);
|
|
3901
|
+
return isUsableWorkflowResolution(resolution) ? resolution.workflow.agent.maxConcurrentAgents : NaN;
|
|
3902
|
+
} catch {
|
|
3903
|
+
return NaN;
|
|
3904
|
+
}
|
|
3905
|
+
}));
|
|
3906
|
+
const validLimits = limits.filter((value) => Number.isFinite(value) && value >= 0);
|
|
3907
|
+
return validLimits.length ? Math.min(...validLimits) : DEFAULT_CONCURRENCY;
|
|
3908
|
+
}
|
|
3909
|
+
async resolveWorkflowResolution(repository, cacheRoot, resolution, changed) {
|
|
3910
|
+
const cacheKey = this.workflowCacheKey(repository);
|
|
3911
|
+
if (resolution.isValid) {
|
|
3912
|
+
const effectiveResolution = {
|
|
3913
|
+
...resolution,
|
|
3914
|
+
isValid: true,
|
|
3915
|
+
usedLastKnownGood: false,
|
|
3916
|
+
validationError: null
|
|
3917
|
+
};
|
|
3918
|
+
let workflowPath = effectiveResolution.workflowPath;
|
|
3919
|
+
try {
|
|
3920
|
+
workflowPath = await this.persistLastKnownGoodWorkflow(cacheRoot, effectiveResolution) ?? effectiveResolution.workflowPath;
|
|
3921
|
+
} catch {
|
|
3922
|
+
workflowPath = effectiveResolution.workflowPath;
|
|
3923
|
+
}
|
|
3924
|
+
this.lastKnownGoodWorkflows.set(cacheKey, {
|
|
3925
|
+
...effectiveResolution,
|
|
3926
|
+
workflowPath
|
|
3927
|
+
});
|
|
3928
|
+
this.lastReportedWorkflowErrors.delete(cacheKey);
|
|
3929
|
+
return effectiveResolution;
|
|
3930
|
+
}
|
|
3931
|
+
const cached = this.lastKnownGoodWorkflows.get(cacheKey);
|
|
3932
|
+
const message = resolution.validationError ?? "Invalid repository WORKFLOW.md";
|
|
3933
|
+
const previousMessage = this.lastReportedWorkflowErrors.get(cacheKey);
|
|
3934
|
+
if (changed || previousMessage !== message) {
|
|
3935
|
+
process.stderr.write(`[orchestrator] failed to reload WORKFLOW.md for ${repository.owner}/${repository.name}: ${message}
|
|
3936
|
+
`);
|
|
3937
|
+
this.lastReportedWorkflowErrors.set(cacheKey, message);
|
|
3938
|
+
}
|
|
3939
|
+
if (!cached) {
|
|
3940
|
+
return resolution;
|
|
3941
|
+
}
|
|
3942
|
+
return {
|
|
3943
|
+
...cached,
|
|
3944
|
+
workflowPath: cached.workflowPath,
|
|
3945
|
+
isValid: false,
|
|
3946
|
+
usedLastKnownGood: true,
|
|
3947
|
+
validationError: message
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
async persistLastKnownGoodWorkflow(cacheRoot, resolution) {
|
|
3951
|
+
if (!resolution.workflowPath) {
|
|
3952
|
+
return null;
|
|
3953
|
+
}
|
|
3954
|
+
const snapshotPath = this.lastKnownGoodWorkflowPath(cacheRoot);
|
|
3955
|
+
const markdown = await readFile5(resolution.workflowPath, "utf8");
|
|
3956
|
+
await mkdir3(join4(cacheRoot, "last-known-good"), { recursive: true });
|
|
3957
|
+
await writeFile3(snapshotPath, markdown, "utf8");
|
|
3958
|
+
return snapshotPath;
|
|
3959
|
+
}
|
|
3960
|
+
lastKnownGoodWorkflowPath(cacheRoot) {
|
|
3961
|
+
return join4(cacheRoot, "last-known-good", "WORKFLOW.md");
|
|
3962
|
+
}
|
|
3963
|
+
workflowCacheKey(repository) {
|
|
3964
|
+
return `${repository.owner}/${repository.name}:${this.normalizeRepositoryCloneUrl(repository.cloneUrl)}`;
|
|
3965
|
+
}
|
|
3966
|
+
normalizeRepositoryCloneUrl(cloneUrl) {
|
|
3967
|
+
if (cloneUrl.startsWith("file://")) {
|
|
3968
|
+
try {
|
|
3969
|
+
return fileURLToPath(cloneUrl);
|
|
3970
|
+
} catch {
|
|
3971
|
+
return cloneUrl;
|
|
3972
|
+
}
|
|
3973
|
+
}
|
|
3974
|
+
return cloneUrl;
|
|
3975
|
+
}
|
|
3976
|
+
isProcessRunning(processId) {
|
|
3977
|
+
if (this.dependencies.isProcessRunning) {
|
|
3978
|
+
return this.dependencies.isProcessRunning(processId);
|
|
3979
|
+
}
|
|
3980
|
+
try {
|
|
3981
|
+
process.kill(-processId, 0);
|
|
3982
|
+
return true;
|
|
3983
|
+
} catch {
|
|
3984
|
+
return false;
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3987
|
+
sendSignal(processId, signal) {
|
|
3988
|
+
try {
|
|
3989
|
+
const kill = this.dependencies.killImpl;
|
|
3990
|
+
if (kill) {
|
|
3991
|
+
kill(processId, signal);
|
|
3992
|
+
} else {
|
|
3993
|
+
process.kill(-processId, signal);
|
|
3994
|
+
}
|
|
3995
|
+
} catch {
|
|
3996
|
+
this.retireWorkerPid(processId);
|
|
3997
|
+
}
|
|
3998
|
+
}
|
|
3999
|
+
pruneExitedWorkerPids() {
|
|
4000
|
+
for (const pid of [...this.activeWorkerPids]) {
|
|
4001
|
+
if (!this.isProcessRunning(pid)) {
|
|
4002
|
+
this.retireWorkerPid(pid);
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
retireWorkerPid(processId) {
|
|
4007
|
+
if (processId) {
|
|
4008
|
+
this.activeWorkerPids.delete(processId);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
/**
|
|
4012
|
+
* Clean up the issue workspace for a terminal issue.
|
|
4013
|
+
*
|
|
4014
|
+
* Runs the `before_remove` hook if configured. Hook failures are logged and
|
|
4015
|
+
* ignored so workspace cleanup still proceeds per spec 9.4. The workspace
|
|
4016
|
+
* directory is removed and the record set to `removed`. Orchestration
|
|
4017
|
+
* records (runs) are preserved.
|
|
4018
|
+
*/
|
|
4019
|
+
async cleanupTerminalIssueWorkspace(tenant, issue, now, workflowResolution) {
|
|
4020
|
+
const issueSubjectId = issue.id;
|
|
4021
|
+
const identity = {
|
|
4022
|
+
projectId: tenant.projectId,
|
|
4023
|
+
adapter: issue.tracker.adapter,
|
|
4024
|
+
issueSubjectId
|
|
4025
|
+
};
|
|
4026
|
+
const preferredWorkspaceKey = deriveIssueWorkspaceKey(identity, issue.identifier);
|
|
4027
|
+
const legacyWorkspaceKey = deriveLegacyIssueWorkspaceKey(identity);
|
|
4028
|
+
const orchestrationRecord = (await this.store.loadProjectIssueOrchestrations(tenant.projectId)).find((record) => record.issueId === issue.id);
|
|
4029
|
+
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));
|
|
4030
|
+
if (!workspaceRecord || workspaceRecord.status === "removed") {
|
|
4031
|
+
return;
|
|
4032
|
+
}
|
|
4033
|
+
const pendingRecord = {
|
|
4034
|
+
...workspaceRecord,
|
|
4035
|
+
status: "cleanup_pending",
|
|
4036
|
+
updatedAt: now.toISOString()
|
|
4037
|
+
};
|
|
4038
|
+
await this.store.saveIssueWorkspace(pendingRecord);
|
|
4039
|
+
const hookResult = await this.runHook("before_remove", tenant, workspaceRecord.repositoryPath, issue.repository, {
|
|
4040
|
+
projectId: tenant.projectId,
|
|
4041
|
+
workspaceKey: workspaceRecord.workspaceKey,
|
|
4042
|
+
issueSubjectId,
|
|
4043
|
+
issueIdentifier: issue.identifier,
|
|
4044
|
+
workspacePath: workspaceRecord.workspacePath,
|
|
4045
|
+
repositoryPath: workspaceRecord.repositoryPath
|
|
4046
|
+
}, workflowResolution);
|
|
4047
|
+
if (hookResult && hookResult.outcome !== "success" && hookResult.outcome !== "skipped") {
|
|
4048
|
+
const errorMessage = hookResult.error ?? `before_remove hook ${hookResult.outcome}`;
|
|
4049
|
+
console.warn(`[orchestrator] before_remove hook failed for ${issue.identifier}; continuing cleanup: ${errorMessage}`);
|
|
4050
|
+
}
|
|
4051
|
+
try {
|
|
4052
|
+
await rm3(workspaceRecord.workspacePath, { recursive: true, force: true });
|
|
4053
|
+
} catch {
|
|
4054
|
+
}
|
|
4055
|
+
const removedRecord = {
|
|
4056
|
+
...workspaceRecord,
|
|
4057
|
+
status: "removed",
|
|
4058
|
+
updatedAt: now.toISOString(),
|
|
4059
|
+
lastError: null
|
|
4060
|
+
};
|
|
4061
|
+
await this.store.saveIssueWorkspace(removedRecord);
|
|
4062
|
+
}
|
|
4063
|
+
};
|
|
4064
|
+
function hasTokenUsage(tokenUsage) {
|
|
4065
|
+
return Boolean(tokenUsage && (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0 || tokenUsage.totalTokens > 0));
|
|
4066
|
+
}
|
|
4067
|
+
function isRecord2(value) {
|
|
4068
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
4069
|
+
}
|
|
4070
|
+
function resolveProjectRateLimits(runs, issues) {
|
|
4071
|
+
let latestRunRateLimits = null;
|
|
4072
|
+
let latestRunTimestamp = -Infinity;
|
|
4073
|
+
for (const run of runs) {
|
|
4074
|
+
if (!isRecord2(run.rateLimits)) {
|
|
4075
|
+
continue;
|
|
4076
|
+
}
|
|
4077
|
+
const timestamp = parseTimestampMs(run.lastEventAt ?? run.updatedAt ?? run.startedAt);
|
|
4078
|
+
const sortableTimestamp = timestamp ?? -Infinity;
|
|
4079
|
+
if (sortableTimestamp >= latestRunTimestamp) {
|
|
4080
|
+
latestRunTimestamp = sortableTimestamp;
|
|
4081
|
+
latestRunRateLimits = run.rateLimits;
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
if (latestRunRateLimits) {
|
|
4085
|
+
return latestRunRateLimits;
|
|
4086
|
+
}
|
|
4087
|
+
for (const issue of issues) {
|
|
4088
|
+
if (isRecord2(issue.rateLimits)) {
|
|
4089
|
+
return issue.rateLimits;
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
return null;
|
|
4093
|
+
}
|
|
4094
|
+
function buildRuntimeSession(existing, sessionId, threadId, status, startedAt, updatedAt, exitClassification = void 0) {
|
|
4095
|
+
if (existing === void 0 && sessionId === null && threadId === null && status === null && (exitClassification === void 0 || exitClassification === null)) {
|
|
4096
|
+
return void 0;
|
|
4097
|
+
}
|
|
4098
|
+
return {
|
|
4099
|
+
sessionId: sessionId ?? existing?.sessionId ?? null,
|
|
4100
|
+
threadId: threadId ?? existing?.threadId ?? null,
|
|
4101
|
+
status: status ?? existing?.status ?? null,
|
|
4102
|
+
startedAt: existing?.startedAt ?? startedAt,
|
|
4103
|
+
updatedAt,
|
|
4104
|
+
exitClassification: exitClassification === void 0 ? existing?.exitClassification ?? null : exitClassification
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
function resolvePersistedCumulativeTurnCount(run) {
|
|
4108
|
+
return run.cumulativeTurnCount ?? run.turnCount ?? 0;
|
|
4109
|
+
}
|
|
4110
|
+
function hasConvergenceLockedRun(runs, issueId, issueState) {
|
|
4111
|
+
const latestRun = runs.filter((run) => run.issueId === issueId).sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime())[0];
|
|
4112
|
+
return latestRun?.runtimeSession?.exitClassification === "convergence-detected" && latestRun.issueState === issueState;
|
|
4113
|
+
}
|
|
4114
|
+
function resolveIssueBudgetSnapshot(runs, issueId) {
|
|
4115
|
+
const issueRuns = runs.filter((run) => run.issueId === issueId);
|
|
4116
|
+
const startedAtCandidates = issueRuns.map((run) => run.startedAt).filter((value) => typeof value === "string");
|
|
4117
|
+
return {
|
|
4118
|
+
cumulativeTurnCount: issueRuns.reduce((total, run) => total + resolvePersistedCumulativeTurnCount(run), 0),
|
|
4119
|
+
tokenUsage: issueRuns.reduce((total, run) => ({
|
|
4120
|
+
inputTokens: total.inputTokens + (run.tokenUsage?.inputTokens ?? 0),
|
|
4121
|
+
outputTokens: total.outputTokens + (run.tokenUsage?.outputTokens ?? 0),
|
|
4122
|
+
totalTokens: total.totalTokens + (run.tokenUsage?.totalTokens ?? 0)
|
|
4123
|
+
}), {
|
|
4124
|
+
inputTokens: 0,
|
|
4125
|
+
outputTokens: 0,
|
|
4126
|
+
totalTokens: 0
|
|
4127
|
+
}),
|
|
4128
|
+
sessionStartedAt: startedAtCandidates.sort((left, right) => left.localeCompare(right))[0] ?? null
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
function isIssueBudgetExceeded(snapshot, now, env = process.env) {
|
|
4132
|
+
const globalMaxTurns = parsePositiveInteger(env.SYMPHONY_GLOBAL_MAX_TURNS ?? "");
|
|
4133
|
+
if (globalMaxTurns !== null && snapshot.cumulativeTurnCount >= globalMaxTurns) {
|
|
4134
|
+
return true;
|
|
4135
|
+
}
|
|
4136
|
+
const maxTokens = parsePositiveInteger(env.SYMPHONY_MAX_TOKENS ?? "");
|
|
4137
|
+
if (maxTokens !== null && snapshot.tokenUsage.totalTokens >= maxTokens) {
|
|
4138
|
+
return true;
|
|
4139
|
+
}
|
|
4140
|
+
const sessionTimeoutMs = parsePositiveInteger(env.SYMPHONY_SESSION_TIMEOUT_MS ?? "");
|
|
4141
|
+
if (sessionTimeoutMs === null || snapshot.sessionStartedAt === null) {
|
|
4142
|
+
return false;
|
|
4143
|
+
}
|
|
4144
|
+
return now.getTime() - new Date(snapshot.sessionStartedAt).getTime() >= sessionTimeoutMs;
|
|
4145
|
+
}
|
|
4146
|
+
function parsePositiveInteger(value) {
|
|
4147
|
+
const parsed = Number(value);
|
|
4148
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
4149
|
+
return null;
|
|
4150
|
+
}
|
|
4151
|
+
return Math.floor(parsed);
|
|
4152
|
+
}
|
|
4153
|
+
function resolveCumulativeTurnCount(run, turnCount) {
|
|
4154
|
+
const carriedTotal = resolvePersistedCumulativeTurnCount(run);
|
|
4155
|
+
if (turnCount === null) {
|
|
4156
|
+
return carriedTotal;
|
|
4157
|
+
}
|
|
4158
|
+
const previousSessionTurnCount = run.turnCount ?? 0;
|
|
4159
|
+
const baseTurnCount = Math.max(0, carriedTotal - previousSessionTurnCount);
|
|
4160
|
+
return baseTurnCount + turnCount;
|
|
4161
|
+
}
|
|
4162
|
+
function isTerminalTurnEvent(event) {
|
|
4163
|
+
return event === "turn/completed" || event === "turn/failed" || event === "turn/cancelled";
|
|
4164
|
+
}
|
|
4165
|
+
function resolveLastTurnSummaryCandidate(event, lastError) {
|
|
4166
|
+
if (typeof lastError === "string" && lastError.trim()) {
|
|
4167
|
+
return lastError.trim();
|
|
4168
|
+
}
|
|
4169
|
+
return typeof event === "string" && isTerminalTurnEvent(event) ? event : null;
|
|
4170
|
+
}
|
|
4171
|
+
function resolveLastTurnSummary(existing, candidate) {
|
|
4172
|
+
if (typeof candidate === "string" && candidate.trim()) {
|
|
4173
|
+
return candidate.trim();
|
|
4174
|
+
}
|
|
4175
|
+
return existing ?? null;
|
|
4176
|
+
}
|
|
4177
|
+
function canApplyWorkerChannelUpdate(status) {
|
|
4178
|
+
return status === "running" || status === "retrying";
|
|
4179
|
+
}
|
|
4180
|
+
function resolveChannelSessionId(sessionInfo) {
|
|
4181
|
+
if (!sessionInfo) {
|
|
4182
|
+
return null;
|
|
4183
|
+
}
|
|
4184
|
+
return sessionInfo.sessionId ?? (sessionInfo.threadId && sessionInfo.turnId ? `${sessionInfo.threadId}-${sessionInfo.turnId}` : null);
|
|
4185
|
+
}
|
|
4186
|
+
function resolveWorkerCommand() {
|
|
4187
|
+
if (process.env.SYMPHONY_WORKER_COMMAND) {
|
|
4188
|
+
return process.env.SYMPHONY_WORKER_COMMAND;
|
|
4189
|
+
}
|
|
4190
|
+
try {
|
|
4191
|
+
const workerUrl = import.meta.resolve("@gh-symphony/worker");
|
|
4192
|
+
return `node ${fileURLToPath(workerUrl)}`;
|
|
4193
|
+
} catch {
|
|
4194
|
+
return DEFAULT_WORKER_COMMAND;
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
function createStore(runtimeRoot = ".runtime", options = {}) {
|
|
4198
|
+
return new OrchestratorFsStore(runtimeRoot, options);
|
|
4199
|
+
}
|
|
4200
|
+
function sortCandidatesForDispatch(candidates) {
|
|
4201
|
+
return [...candidates].sort((a, b) => {
|
|
4202
|
+
if (a.priority !== b.priority) {
|
|
4203
|
+
if (a.priority === null)
|
|
4204
|
+
return 1;
|
|
4205
|
+
if (b.priority === null)
|
|
4206
|
+
return -1;
|
|
4207
|
+
return a.priority - b.priority;
|
|
4208
|
+
}
|
|
4209
|
+
if (a.createdAt !== b.createdAt) {
|
|
4210
|
+
if (a.createdAt === null)
|
|
4211
|
+
return 1;
|
|
4212
|
+
if (b.createdAt === null)
|
|
4213
|
+
return -1;
|
|
4214
|
+
return a.createdAt < b.createdAt ? -1 : 1;
|
|
4215
|
+
}
|
|
4216
|
+
return a.identifier.localeCompare(b.identifier);
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
4219
|
+
function createProjectItemsCache() {
|
|
4220
|
+
const entries = /* @__PURE__ */ new Map();
|
|
4221
|
+
return {
|
|
4222
|
+
getOrLoad(key, load) {
|
|
4223
|
+
const cached = entries.get(key);
|
|
4224
|
+
if (cached) {
|
|
4225
|
+
return cached;
|
|
4226
|
+
}
|
|
4227
|
+
const pending = load().catch((error) => {
|
|
4228
|
+
entries.delete(key);
|
|
4229
|
+
throw error;
|
|
4230
|
+
});
|
|
4231
|
+
entries.set(key, pending);
|
|
4232
|
+
return pending;
|
|
4233
|
+
}
|
|
4234
|
+
};
|
|
4235
|
+
}
|
|
4236
|
+
function wait2(ms) {
|
|
4237
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
4238
|
+
}
|
|
4239
|
+
function createRunId(now, projectId, issueIdentifier) {
|
|
4240
|
+
return [
|
|
4241
|
+
projectId,
|
|
4242
|
+
issueIdentifier.replace(/[^a-zA-Z0-9]+/g, "-"),
|
|
4243
|
+
now.getTime().toString(36)
|
|
4244
|
+
].join("-");
|
|
4245
|
+
}
|
|
4246
|
+
function isIssueOrchestrationClaimed(state) {
|
|
4247
|
+
return state === "claimed" || state === "running" || state === "retry_queued";
|
|
4248
|
+
}
|
|
4249
|
+
function upsertIssueOrchestration(issueRecords, nextRecord) {
|
|
4250
|
+
const existingRecord = issueRecords.find((record) => record.issueId === nextRecord.issueId) ?? null;
|
|
4251
|
+
const remaining = issueRecords.filter((record) => record.issueId !== nextRecord.issueId);
|
|
4252
|
+
return [
|
|
4253
|
+
...remaining,
|
|
4254
|
+
{
|
|
4255
|
+
...nextRecord,
|
|
4256
|
+
completedOnce: nextRecord.completedOnce ?? existingRecord?.completedOnce ?? false
|
|
4257
|
+
}
|
|
4258
|
+
];
|
|
4259
|
+
}
|
|
4260
|
+
function releaseIssueOrchestration(issueRecords, issueId, now) {
|
|
4261
|
+
return issueRecords.map((record) => record.issueId === issueId ? {
|
|
4262
|
+
...record,
|
|
4263
|
+
state: "released",
|
|
4264
|
+
currentRunId: null,
|
|
4265
|
+
retryEntry: null,
|
|
4266
|
+
updatedAt: now.toISOString()
|
|
4267
|
+
} : record);
|
|
4268
|
+
}
|
|
4269
|
+
function isActiveRunStatus(status) {
|
|
4270
|
+
return status === "pending" || status === "starting" || status === "running" || status === "retrying";
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
// ../orchestrator/dist/lock.js
|
|
4274
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
4275
|
+
import { mkdir as mkdir4, open as open2, readFile as readFile6, rm as rm4 } from "fs/promises";
|
|
4276
|
+
import { dirname as dirname2, isAbsolute, join as join5, relative as relative2, resolve as resolve4 } from "path";
|
|
4277
|
+
import { setTimeout as delay } from "timers/promises";
|
|
4278
|
+
var LOCK_READ_RETRY_DELAY_MS = 10;
|
|
4279
|
+
var LOCK_READ_RETRY_LIMIT = 20;
|
|
4280
|
+
async function acquireProjectLock(input) {
|
|
4281
|
+
assertValidProjectId(input.projectId);
|
|
4282
|
+
const pid = input.pid ?? process.pid;
|
|
4283
|
+
const startedAt = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
4284
|
+
const ownerToken = `${pid}:${randomUUID2()}`;
|
|
4285
|
+
const lockPath = resolveProjectLockPath(input.runtimeRoot, input.projectId);
|
|
4286
|
+
const record = { ownerToken, pid, startedAt };
|
|
4287
|
+
let invalidReadAttempts = 0;
|
|
4288
|
+
for (; ; ) {
|
|
4289
|
+
try {
|
|
4290
|
+
await mkdir4(dirname2(lockPath), { recursive: true });
|
|
4291
|
+
const handle = await open2(lockPath, "wx");
|
|
4292
|
+
try {
|
|
4293
|
+
await handle.writeFile(JSON.stringify(record, null, 2) + "\n", "utf8");
|
|
4294
|
+
} finally {
|
|
4295
|
+
await handle.close();
|
|
4296
|
+
}
|
|
4297
|
+
return { lockPath, ownerToken, pid, startedAt };
|
|
4298
|
+
} catch (error) {
|
|
4299
|
+
if (!isAlreadyExistsError2(error)) {
|
|
4300
|
+
throw error;
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
const existing = await readProjectLock(lockPath);
|
|
4304
|
+
if (existing.status === "missing") {
|
|
4305
|
+
invalidReadAttempts = 0;
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
if (existing.status === "invalid") {
|
|
4309
|
+
invalidReadAttempts += 1;
|
|
4310
|
+
if (invalidReadAttempts >= LOCK_READ_RETRY_LIMIT) {
|
|
4311
|
+
throw new Error(`Project "${input.projectId}" lock file is unreadable at "${lockPath}".`);
|
|
4312
|
+
}
|
|
4313
|
+
await delay(LOCK_READ_RETRY_DELAY_MS);
|
|
4314
|
+
continue;
|
|
4315
|
+
}
|
|
4316
|
+
invalidReadAttempts = 0;
|
|
4317
|
+
if ((input.isProcessRunning ?? isProcessRunning)(existing.record.pid)) {
|
|
4318
|
+
throw new Error(`Project "${input.projectId}" is already running (PID ${existing.record.pid}).`);
|
|
4319
|
+
}
|
|
4320
|
+
await rm4(lockPath, { force: true });
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
async function releaseProjectLock(lock) {
|
|
4324
|
+
if (!lock) {
|
|
4325
|
+
return;
|
|
4326
|
+
}
|
|
4327
|
+
try {
|
|
4328
|
+
const existing = await readProjectLock(lock.lockPath);
|
|
4329
|
+
if (existing.status !== "valid" || existing.record.ownerToken !== lock.ownerToken) {
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
4332
|
+
} catch (error) {
|
|
4333
|
+
if (isMissingFileError2(error)) {
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
throw error;
|
|
4337
|
+
}
|
|
4338
|
+
await rm4(lock.lockPath, { force: true });
|
|
4339
|
+
}
|
|
4340
|
+
async function readProjectLock(lockPath) {
|
|
4341
|
+
try {
|
|
4342
|
+
const raw = await readFile6(lockPath, "utf8");
|
|
4343
|
+
const record = parseProjectLock(raw);
|
|
4344
|
+
if (!record) {
|
|
4345
|
+
return { status: "invalid" };
|
|
4346
|
+
}
|
|
4347
|
+
return { status: "valid", record };
|
|
4348
|
+
} catch (error) {
|
|
4349
|
+
if (isMissingFileError2(error)) {
|
|
4350
|
+
return { status: "missing" };
|
|
4351
|
+
}
|
|
4352
|
+
throw error;
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
function assertValidProjectId(projectId) {
|
|
4356
|
+
if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
|
|
4357
|
+
throw new Error(`Invalid project ID "${projectId}". Project IDs must not contain path separators or traversal segments.`);
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
function resolveProjectLockPath(runtimeRoot, projectId) {
|
|
4361
|
+
const store = new OrchestratorFsStore(runtimeRoot);
|
|
4362
|
+
const projectsRoot = resolve4(runtimeRoot, "projects");
|
|
4363
|
+
const projectDir = resolve4(store.projectDir(projectId));
|
|
4364
|
+
const relativeProjectDir = relative2(projectsRoot, projectDir);
|
|
4365
|
+
if (relativeProjectDir.length === 0 || relativeProjectDir.startsWith("..") || isAbsolute(relativeProjectDir)) {
|
|
4366
|
+
throw new Error(`Invalid project ID "${projectId}". Project lock path must stay within "${projectsRoot}".`);
|
|
4367
|
+
}
|
|
4368
|
+
return join5(projectDir, ".lock");
|
|
4369
|
+
}
|
|
4370
|
+
function parseProjectLock(raw) {
|
|
4371
|
+
try {
|
|
4372
|
+
const parsed = JSON.parse(raw);
|
|
4373
|
+
if (typeof parsed.ownerToken !== "string" || typeof parsed.pid !== "number" || !Number.isInteger(parsed.pid) || parsed.pid <= 0 || typeof parsed.startedAt !== "string") {
|
|
4374
|
+
return null;
|
|
4375
|
+
}
|
|
4376
|
+
return {
|
|
4377
|
+
ownerToken: parsed.ownerToken,
|
|
4378
|
+
pid: parsed.pid,
|
|
4379
|
+
startedAt: parsed.startedAt
|
|
4380
|
+
};
|
|
4381
|
+
} catch {
|
|
4382
|
+
return null;
|
|
4383
|
+
}
|
|
4384
|
+
}
|
|
4385
|
+
function isProcessRunning(pid) {
|
|
4386
|
+
try {
|
|
4387
|
+
process.kill(pid, 0);
|
|
4388
|
+
return true;
|
|
4389
|
+
} catch (error) {
|
|
4390
|
+
return !(error && typeof error === "object" && "code" in error && error.code === "ESRCH");
|
|
4391
|
+
}
|
|
4392
|
+
}
|
|
4393
|
+
function isAlreadyExistsError2(error) {
|
|
4394
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
|
4395
|
+
}
|
|
4396
|
+
function isMissingFileError2(error) {
|
|
4397
|
+
return Boolean(error && typeof error === "object" && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"));
|
|
4398
|
+
}
|
|
4399
|
+
|
|
4400
|
+
// ../orchestrator/dist/index.js
|
|
4401
|
+
import { pathToFileURL } from "url";
|
|
4402
|
+
import { resolve as resolve5 } from "path";
|
|
4403
|
+
function resolveOrchestratorLogLevel(value) {
|
|
4404
|
+
if (!value || value === "normal") {
|
|
4405
|
+
return "normal";
|
|
4406
|
+
}
|
|
4407
|
+
if (value === "verbose") {
|
|
4408
|
+
return "verbose";
|
|
4409
|
+
}
|
|
4410
|
+
throw new Error(`Unsupported log level: ${value}. Supported values: normal, verbose.`);
|
|
4411
|
+
}
|
|
4412
|
+
async function runCli(argv, dependencies = {}) {
|
|
4413
|
+
const [command = "run-once", ...args] = argv;
|
|
4414
|
+
const parsed = parseArgs(args);
|
|
4415
|
+
if (parsed.projectId) {
|
|
4416
|
+
assertValidProjectId(parsed.projectId);
|
|
4417
|
+
}
|
|
4418
|
+
const runtimeRoot = resolve5(parsed.runtimeRoot ?? ".runtime");
|
|
4419
|
+
const stderr = dependencies.stderr ?? process.stderr;
|
|
4420
|
+
const eventsDir = resolveOptionalPath(parsed.eventsDir ?? process.env.SYMPHONY_EVENTS_DIR);
|
|
4421
|
+
const logLevel = resolveOrchestratorLogLevel(parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL);
|
|
4422
|
+
const service = await dependencies.createService?.(runtimeRoot, parsed.projectId, {
|
|
4423
|
+
eventsDir,
|
|
4424
|
+
logLevel,
|
|
4425
|
+
stderr
|
|
4426
|
+
}) ?? await createServiceForRuntime(runtimeRoot, parsed.projectId, {
|
|
4427
|
+
eventsDir,
|
|
4428
|
+
logLevel,
|
|
4429
|
+
stderr
|
|
4430
|
+
});
|
|
4431
|
+
const stdout = dependencies.stdout ?? process.stdout;
|
|
4432
|
+
const exitProcess = dependencies.exitProcess ?? process.exit;
|
|
4433
|
+
const signalTarget = dependencies.signalTarget ?? process;
|
|
4434
|
+
switch (command) {
|
|
4435
|
+
case "run": {
|
|
4436
|
+
let lock = null;
|
|
4437
|
+
let cleanupPromise = null;
|
|
4438
|
+
let shuttingDownForSignal = false;
|
|
4439
|
+
const cleanup = async () => {
|
|
4440
|
+
if (cleanupPromise) {
|
|
4441
|
+
return cleanupPromise;
|
|
4442
|
+
}
|
|
4443
|
+
cleanupPromise = (async () => {
|
|
4444
|
+
let cleanupError;
|
|
4445
|
+
const shutdownPromise = service.shutdown();
|
|
4446
|
+
try {
|
|
4447
|
+
await shutdownPromise;
|
|
4448
|
+
} catch (error) {
|
|
4449
|
+
cleanupError = error;
|
|
4450
|
+
} finally {
|
|
4451
|
+
try {
|
|
4452
|
+
await (dependencies.releaseLock ?? releaseProjectLock)(lock);
|
|
4453
|
+
lock = null;
|
|
4454
|
+
} catch (lockError) {
|
|
4455
|
+
cleanupError ??= lockError;
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
if (cleanupError) {
|
|
4459
|
+
throw cleanupError;
|
|
4460
|
+
}
|
|
4461
|
+
})();
|
|
4462
|
+
return cleanupPromise;
|
|
4463
|
+
};
|
|
4464
|
+
const handleSignal = (signal) => {
|
|
4465
|
+
shuttingDownForSignal = true;
|
|
4466
|
+
let exitCode = 0;
|
|
4467
|
+
void cleanup().catch((error) => {
|
|
4468
|
+
exitCode = 1;
|
|
4469
|
+
stderr.write(`Failed to shut down orchestrator after ${signal}: ${error instanceof Error ? error.message : String(error)}
|
|
4470
|
+
`);
|
|
4471
|
+
}).finally(() => {
|
|
4472
|
+
exitProcess(exitCode);
|
|
4473
|
+
});
|
|
4474
|
+
};
|
|
4475
|
+
const sigintHandler = () => handleSignal("SIGINT");
|
|
4476
|
+
const sigtermHandler = () => handleSignal("SIGTERM");
|
|
4477
|
+
try {
|
|
4478
|
+
if (parsed.projectId) {
|
|
4479
|
+
lock = await (dependencies.acquireLock ?? acquireProjectLock)({
|
|
4480
|
+
runtimeRoot,
|
|
4481
|
+
projectId: parsed.projectId
|
|
4482
|
+
});
|
|
4483
|
+
}
|
|
4484
|
+
signalTarget.once("SIGINT", sigintHandler);
|
|
4485
|
+
signalTarget.once("SIGTERM", sigtermHandler);
|
|
4486
|
+
await service.run({
|
|
4487
|
+
issueIdentifier: parsed.issueIdentifier
|
|
4488
|
+
});
|
|
4489
|
+
await cleanup();
|
|
4490
|
+
} finally {
|
|
4491
|
+
signalTarget.off("SIGINT", sigintHandler);
|
|
4492
|
+
signalTarget.off("SIGTERM", sigtermHandler);
|
|
4493
|
+
if (!shuttingDownForSignal) {
|
|
4494
|
+
await cleanup();
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
return;
|
|
4498
|
+
}
|
|
4499
|
+
case "run-once":
|
|
4500
|
+
case "dispatch": {
|
|
4501
|
+
const result = await service.runOnce({
|
|
4502
|
+
issueIdentifier: parsed.issueIdentifier
|
|
4503
|
+
});
|
|
4504
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
4505
|
+
return;
|
|
4506
|
+
}
|
|
4507
|
+
case "run-issue": {
|
|
4508
|
+
if (!parsed.projectId || !parsed.issueIdentifier) {
|
|
4509
|
+
throw new Error("run-issue requires --project-id and --issue.");
|
|
4510
|
+
}
|
|
4511
|
+
const result = await service.runOnce({
|
|
4512
|
+
issueIdentifier: parsed.issueIdentifier
|
|
4513
|
+
});
|
|
4514
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
4515
|
+
return;
|
|
4516
|
+
}
|
|
4517
|
+
case "recover": {
|
|
4518
|
+
const result = await service.recover();
|
|
4519
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
case "status": {
|
|
4523
|
+
const result = await service.status();
|
|
4524
|
+
stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
default:
|
|
4528
|
+
throw new Error(`Unsupported command: ${command}`);
|
|
4529
|
+
}
|
|
4530
|
+
}
|
|
4531
|
+
async function createServiceForRuntime(runtimeRoot, projectId, options) {
|
|
4532
|
+
if (!projectId) {
|
|
4533
|
+
throw new Error("Orchestrator CLI requires --project-id.");
|
|
4534
|
+
}
|
|
4535
|
+
const store = createStore(runtimeRoot, {
|
|
4536
|
+
eventsMirrorRoot: options?.eventsDir
|
|
4537
|
+
});
|
|
4538
|
+
const projectConfig = await store.loadProjectConfig(projectId);
|
|
4539
|
+
if (!projectConfig) {
|
|
4540
|
+
throw new Error(`Project config not found for "${projectId}".`);
|
|
4541
|
+
}
|
|
4542
|
+
return new OrchestratorService(store, projectConfig, options);
|
|
4543
|
+
}
|
|
4544
|
+
async function main() {
|
|
4545
|
+
await runCli(process.argv.slice(2));
|
|
4546
|
+
}
|
|
4547
|
+
function parseArgs(args) {
|
|
4548
|
+
const parsed = {};
|
|
4549
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
4550
|
+
const argument = args[index];
|
|
4551
|
+
const value = args[index + 1];
|
|
4552
|
+
if (!argument?.startsWith("--")) {
|
|
4553
|
+
continue;
|
|
4554
|
+
}
|
|
4555
|
+
switch (argument) {
|
|
4556
|
+
case "--runtime-root":
|
|
4557
|
+
parsed.runtimeRoot = value;
|
|
4558
|
+
index += 1;
|
|
4559
|
+
break;
|
|
4560
|
+
case "--project":
|
|
4561
|
+
case "--project-id":
|
|
4562
|
+
parsed.projectId = value;
|
|
4563
|
+
index += 1;
|
|
4564
|
+
break;
|
|
4565
|
+
case "--issue":
|
|
4566
|
+
parsed.issueIdentifier = value;
|
|
4567
|
+
index += 1;
|
|
4568
|
+
break;
|
|
4569
|
+
case "--events-dir":
|
|
4570
|
+
if (!value || value.startsWith("-")) {
|
|
4571
|
+
throw new Error(`Option '${argument}' argument missing`);
|
|
4572
|
+
}
|
|
4573
|
+
parsed.eventsDir = value;
|
|
4574
|
+
index += 1;
|
|
4575
|
+
break;
|
|
4576
|
+
case "--log-level":
|
|
4577
|
+
if (!value || value.startsWith("-")) {
|
|
4578
|
+
throw new Error(`Option '${argument}' argument missing`);
|
|
4579
|
+
}
|
|
4580
|
+
parsed.logLevel = value;
|
|
4581
|
+
index += 1;
|
|
4582
|
+
break;
|
|
4583
|
+
default:
|
|
4584
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
4585
|
+
}
|
|
4586
|
+
}
|
|
4587
|
+
return parsed;
|
|
4588
|
+
}
|
|
4589
|
+
function resolveOptionalPath(value) {
|
|
4590
|
+
if (!value || value.trim().length === 0) {
|
|
4591
|
+
return void 0;
|
|
4592
|
+
}
|
|
4593
|
+
return resolve5(value.trim());
|
|
4594
|
+
}
|
|
4595
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
4596
|
+
main().catch((error) => {
|
|
4597
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Unknown error"}
|
|
4598
|
+
`);
|
|
4599
|
+
process.exitCode = 1;
|
|
4600
|
+
});
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
export {
|
|
4604
|
+
deriveIssueWorkspaceKeyFromIdentifier,
|
|
4605
|
+
readJsonFile,
|
|
4606
|
+
safeReadDir,
|
|
4607
|
+
isFileMissing,
|
|
4608
|
+
parseRecentEvents,
|
|
4609
|
+
isMatchingIssueRun,
|
|
4610
|
+
mapIssueOrchestrationStateToStatus,
|
|
4611
|
+
OrchestratorService,
|
|
4612
|
+
createStore,
|
|
4613
|
+
acquireProjectLock,
|
|
4614
|
+
releaseProjectLock,
|
|
4615
|
+
resolveOrchestratorLogLevel,
|
|
4616
|
+
runCli
|
|
4617
|
+
};
|