@gh-symphony/cli 0.0.21 → 0.0.22

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