@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.
Files changed (100) hide show
  1. package/dist/chunk-5NV3LSAJ.js +11 -0
  2. package/dist/chunk-6HBZC3BE.js +468 -0
  3. package/dist/chunk-76QPITKI.js +109 -0
  4. package/dist/chunk-IWR4UQEJ.js +2250 -0
  5. package/dist/chunk-JO3AXHQI.js +130 -0
  6. package/dist/chunk-M7OSMUTN.js +874 -0
  7. package/dist/chunk-MVRF7BES.js +68 -0
  8. package/dist/chunk-RNWX7DQU.js +4617 -0
  9. package/dist/chunk-ROGRTUFI.js +108 -0
  10. package/dist/chunk-TH5QPO3Y.js +67 -0
  11. package/dist/config-cmd-AZ7POMAA.js +110 -0
  12. package/dist/index.d.ts +5 -4
  13. package/dist/index.js +568 -356
  14. package/dist/init-EZXQAXZM.js +17 -0
  15. package/dist/logs-6LNGT2GF.js +188 -0
  16. package/dist/project-3ELXQ35D.js +678 -0
  17. package/dist/recover-T6ME6C56.js +130 -0
  18. package/dist/repo-R3XBIVAX.js +121 -0
  19. package/dist/run-DYINRZHK.js +107 -0
  20. package/dist/start-PIFQMIC2.js +15 -0
  21. package/dist/status-3WK5BWRZ.js +11 -0
  22. package/dist/stop-AA3AP5M6.js +9 -0
  23. package/dist/version-VBB62JWI.js +30 -0
  24. package/package.json +11 -6
  25. package/dist/ansi.d.ts +0 -15
  26. package/dist/ansi.js +0 -53
  27. package/dist/commands/config-cmd.d.ts +0 -3
  28. package/dist/commands/config-cmd.js +0 -90
  29. package/dist/commands/help.d.ts +0 -3
  30. package/dist/commands/help.js +0 -55
  31. package/dist/commands/init.d.ts +0 -34
  32. package/dist/commands/init.js +0 -477
  33. package/dist/commands/logs.d.ts +0 -3
  34. package/dist/commands/logs.js +0 -184
  35. package/dist/commands/project.d.ts +0 -3
  36. package/dist/commands/project.js +0 -649
  37. package/dist/commands/recover.d.ts +0 -3
  38. package/dist/commands/recover.js +0 -119
  39. package/dist/commands/repo.d.ts +0 -3
  40. package/dist/commands/repo.js +0 -103
  41. package/dist/commands/run.d.ts +0 -3
  42. package/dist/commands/run.js +0 -95
  43. package/dist/commands/start.d.ts +0 -20
  44. package/dist/commands/start.js +0 -344
  45. package/dist/commands/status-refresh.d.ts +0 -9
  46. package/dist/commands/status-refresh.js +0 -27
  47. package/dist/commands/status.d.ts +0 -3
  48. package/dist/commands/status.js +0 -237
  49. package/dist/commands/stop.d.ts +0 -3
  50. package/dist/commands/stop.js +0 -92
  51. package/dist/commands/version.d.ts +0 -3
  52. package/dist/commands/version.js +0 -21
  53. package/dist/completion.d.ts +0 -1
  54. package/dist/completion.js +0 -204
  55. package/dist/config.d.ts +0 -38
  56. package/dist/config.js +0 -82
  57. package/dist/context/context-types.d.ts +0 -36
  58. package/dist/context/context-types.js +0 -1
  59. package/dist/context/generate-context-yaml.d.ts +0 -15
  60. package/dist/context/generate-context-yaml.js +0 -129
  61. package/dist/dashboard/renderer.d.ts +0 -9
  62. package/dist/dashboard/renderer.js +0 -220
  63. package/dist/detection/environment-detector.d.ts +0 -11
  64. package/dist/detection/environment-detector.js +0 -140
  65. package/dist/github/client.d.ts +0 -71
  66. package/dist/github/client.js +0 -348
  67. package/dist/github/gh-auth.d.ts +0 -34
  68. package/dist/github/gh-auth.js +0 -110
  69. package/dist/mapping/smart-defaults.d.ts +0 -17
  70. package/dist/mapping/smart-defaults.js +0 -86
  71. package/dist/orchestrator-runtime.d.ts +0 -1
  72. package/dist/orchestrator-runtime.js +0 -4
  73. package/dist/orchestrator-status-endpoint.d.ts +0 -5
  74. package/dist/orchestrator-status-endpoint.js +0 -27
  75. package/dist/project-selection.d.ts +0 -8
  76. package/dist/project-selection.js +0 -56
  77. package/dist/skills/skill-writer.d.ts +0 -14
  78. package/dist/skills/skill-writer.js +0 -62
  79. package/dist/skills/templates/commit.d.ts +0 -2
  80. package/dist/skills/templates/commit.js +0 -45
  81. package/dist/skills/templates/document.d.ts +0 -7
  82. package/dist/skills/templates/document.js +0 -16
  83. package/dist/skills/templates/gh-project.d.ts +0 -2
  84. package/dist/skills/templates/gh-project.js +0 -88
  85. package/dist/skills/templates/gh-symphony.d.ts +0 -2
  86. package/dist/skills/templates/gh-symphony.js +0 -125
  87. package/dist/skills/templates/index.d.ts +0 -8
  88. package/dist/skills/templates/index.js +0 -28
  89. package/dist/skills/templates/land.d.ts +0 -2
  90. package/dist/skills/templates/land.js +0 -59
  91. package/dist/skills/templates/pull.d.ts +0 -2
  92. package/dist/skills/templates/pull.js +0 -41
  93. package/dist/skills/templates/push.d.ts +0 -2
  94. package/dist/skills/templates/push.js +0 -36
  95. package/dist/skills/types.d.ts +0 -23
  96. package/dist/skills/types.js +0 -1
  97. package/dist/workflow/generate-reference-workflow.d.ts +0 -9
  98. package/dist/workflow/generate-reference-workflow.js +0 -261
  99. package/dist/workflow/generate-workflow-md.d.ts +0 -12
  100. 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
+ };