@gh-symphony/cli 0.0.20 → 0.0.22

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