@gh-symphony/cli 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/dist/chunk-5NV3LSAJ.js +11 -0
  2. package/dist/chunk-6HBZC3BE.js +468 -0
  3. package/dist/chunk-76QPITKI.js +109 -0
  4. package/dist/chunk-EFMFGOWM.js +3575 -0
  5. package/dist/chunk-IWR4UQEJ.js +2250 -0
  6. package/dist/chunk-JO3AXHQI.js +130 -0
  7. package/dist/chunk-MHIWAIVD.js +876 -0
  8. package/dist/chunk-MVRF7BES.js +68 -0
  9. package/dist/chunk-ROGRTUFI.js +108 -0
  10. package/dist/chunk-TF3QNWNC.js +1121 -0
  11. package/dist/chunk-TH5QPO3Y.js +67 -0
  12. package/dist/config-cmd-AZ7POMAA.js +110 -0
  13. package/dist/index.d.ts +5 -4
  14. package/dist/index.js +568 -356
  15. package/dist/init-EZXQAXZM.js +17 -0
  16. package/dist/logs-6LNGT2GF.js +188 -0
  17. package/dist/project-557FE2GD.js +679 -0
  18. package/dist/recover-LVBI2TGH.js +131 -0
  19. package/dist/repo-R3XBIVAX.js +121 -0
  20. package/dist/run-WITYAYFZ.js +108 -0
  21. package/dist/start-JUFKNL3N.js +16 -0
  22. package/dist/status-3WK5BWRZ.js +11 -0
  23. package/dist/stop-AA3AP5M6.js +9 -0
  24. package/dist/version-VBB62JWI.js +30 -0
  25. package/dist/worker-entry.js +1828 -0
  26. package/package.json +9 -4
  27. package/dist/ansi.d.ts +0 -15
  28. package/dist/ansi.js +0 -53
  29. package/dist/commands/config-cmd.d.ts +0 -3
  30. package/dist/commands/config-cmd.js +0 -90
  31. package/dist/commands/help.d.ts +0 -3
  32. package/dist/commands/help.js +0 -55
  33. package/dist/commands/init.d.ts +0 -34
  34. package/dist/commands/init.js +0 -477
  35. package/dist/commands/logs.d.ts +0 -3
  36. package/dist/commands/logs.js +0 -184
  37. package/dist/commands/project.d.ts +0 -3
  38. package/dist/commands/project.js +0 -649
  39. package/dist/commands/recover.d.ts +0 -3
  40. package/dist/commands/recover.js +0 -119
  41. package/dist/commands/repo.d.ts +0 -3
  42. package/dist/commands/repo.js +0 -103
  43. package/dist/commands/run.d.ts +0 -3
  44. package/dist/commands/run.js +0 -95
  45. package/dist/commands/start.d.ts +0 -20
  46. package/dist/commands/start.js +0 -344
  47. package/dist/commands/status-refresh.d.ts +0 -9
  48. package/dist/commands/status-refresh.js +0 -27
  49. package/dist/commands/status.d.ts +0 -3
  50. package/dist/commands/status.js +0 -237
  51. package/dist/commands/stop.d.ts +0 -3
  52. package/dist/commands/stop.js +0 -92
  53. package/dist/commands/version.d.ts +0 -3
  54. package/dist/commands/version.js +0 -21
  55. package/dist/completion.d.ts +0 -1
  56. package/dist/completion.js +0 -204
  57. package/dist/config.d.ts +0 -38
  58. package/dist/config.js +0 -82
  59. package/dist/context/context-types.d.ts +0 -36
  60. package/dist/context/context-types.js +0 -1
  61. package/dist/context/generate-context-yaml.d.ts +0 -15
  62. package/dist/context/generate-context-yaml.js +0 -129
  63. package/dist/dashboard/renderer.d.ts +0 -9
  64. package/dist/dashboard/renderer.js +0 -220
  65. package/dist/detection/environment-detector.d.ts +0 -11
  66. package/dist/detection/environment-detector.js +0 -140
  67. package/dist/github/client.d.ts +0 -71
  68. package/dist/github/client.js +0 -348
  69. package/dist/github/gh-auth.d.ts +0 -34
  70. package/dist/github/gh-auth.js +0 -110
  71. package/dist/mapping/smart-defaults.d.ts +0 -17
  72. package/dist/mapping/smart-defaults.js +0 -86
  73. package/dist/orchestrator-runtime.d.ts +0 -1
  74. package/dist/orchestrator-runtime.js +0 -4
  75. package/dist/orchestrator-status-endpoint.d.ts +0 -5
  76. package/dist/orchestrator-status-endpoint.js +0 -27
  77. package/dist/project-selection.d.ts +0 -8
  78. package/dist/project-selection.js +0 -56
  79. package/dist/skills/skill-writer.d.ts +0 -14
  80. package/dist/skills/skill-writer.js +0 -62
  81. package/dist/skills/templates/commit.d.ts +0 -2
  82. package/dist/skills/templates/commit.js +0 -45
  83. package/dist/skills/templates/document.d.ts +0 -7
  84. package/dist/skills/templates/document.js +0 -16
  85. package/dist/skills/templates/gh-project.d.ts +0 -2
  86. package/dist/skills/templates/gh-project.js +0 -88
  87. package/dist/skills/templates/gh-symphony.d.ts +0 -2
  88. package/dist/skills/templates/gh-symphony.js +0 -125
  89. package/dist/skills/templates/index.d.ts +0 -8
  90. package/dist/skills/templates/index.js +0 -28
  91. package/dist/skills/templates/land.d.ts +0 -2
  92. package/dist/skills/templates/land.js +0 -59
  93. package/dist/skills/templates/pull.d.ts +0 -2
  94. package/dist/skills/templates/pull.js +0 -41
  95. package/dist/skills/templates/push.d.ts +0 -2
  96. package/dist/skills/templates/push.js +0 -36
  97. package/dist/skills/types.d.ts +0 -23
  98. package/dist/skills/types.js +0 -1
  99. package/dist/workflow/generate-reference-workflow.d.ts +0 -9
  100. package/dist/workflow/generate-reference-workflow.js +0 -261
  101. package/dist/workflow/generate-workflow-md.d.ts +0 -12
  102. package/dist/workflow/generate-workflow-md.js +0 -134
@@ -0,0 +1,876 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getGhToken
4
+ } from "./chunk-JO3AXHQI.js";
5
+ import {
6
+ bold,
7
+ cyan,
8
+ dim,
9
+ green,
10
+ red,
11
+ setNoColor,
12
+ yellow
13
+ } from "./chunk-MVRF7BES.js";
14
+ import {
15
+ OrchestratorService,
16
+ acquireProjectLock,
17
+ createStore,
18
+ releaseProjectLock,
19
+ resolveOrchestratorLogLevel
20
+ } from "./chunk-EFMFGOWM.js";
21
+ import {
22
+ deriveIssueWorkspaceKeyFromIdentifier,
23
+ isFileMissing,
24
+ isMatchingIssueRun,
25
+ mapIssueOrchestrationStateToStatus,
26
+ parseRecentEvents,
27
+ readJsonFile,
28
+ safeReadDir
29
+ } from "./chunk-TF3QNWNC.js";
30
+ import {
31
+ resolveRuntimeRoot
32
+ } from "./chunk-5NV3LSAJ.js";
33
+ import {
34
+ handleMissingManagedProjectConfig,
35
+ resolveManagedProjectConfig
36
+ } from "./chunk-TH5QPO3Y.js";
37
+ import {
38
+ daemonPidPath,
39
+ httpStatusPath,
40
+ orchestratorLogPath,
41
+ writeJsonFile
42
+ } from "./chunk-ROGRTUFI.js";
43
+
44
+ // src/commands/start.ts
45
+ import { writeFile, mkdir, readFile, rm } from "fs/promises";
46
+ import { dirname, join as join2 } from "path";
47
+ import { spawn } from "child_process";
48
+ import { createServer as createServer2 } from "http";
49
+
50
+ // ../dashboard/dist/store.js
51
+ import { open } from "fs/promises";
52
+ import { join, resolve } from "path";
53
+ var DEFAULT_RECENT_EVENT_LIMIT = 20;
54
+ var RECENT_EVENT_CHUNK_SIZE = 4096;
55
+ var MAX_RECENT_EVENT_SCAN_BYTES = 64 * 1024;
56
+ var RUN_RECORD_LOAD_CONCURRENCY = 8;
57
+ var DashboardFsReader = class {
58
+ runtimeRoot;
59
+ projectId;
60
+ resolvedRuntimeRoot;
61
+ constructor(runtimeRoot, projectId) {
62
+ this.runtimeRoot = runtimeRoot;
63
+ this.projectId = projectId;
64
+ assertValidDashboardProjectId(projectId);
65
+ this.resolvedRuntimeRoot = resolve(runtimeRoot);
66
+ }
67
+ projectDir() {
68
+ return join(this.resolvedRuntimeRoot, "projects", this.projectId);
69
+ }
70
+ runDir(runId) {
71
+ assertValidDashboardRunId(runId);
72
+ return join(this.projectDir(), "runs", runId);
73
+ }
74
+ async loadProjectStatus() {
75
+ return readJsonFile(join(this.projectDir(), "status.json"));
76
+ }
77
+ async loadProjectState() {
78
+ const snapshot = await this.loadProjectStatus();
79
+ if (!snapshot) {
80
+ return null;
81
+ }
82
+ const issues = await this.loadProjectIssueOrchestrations();
83
+ return {
84
+ ...snapshot,
85
+ completedCount: issues.filter((issue) => issue.completedOnce).length,
86
+ issues
87
+ };
88
+ }
89
+ async loadProjectIssueOrchestrations() {
90
+ const issues = await readJsonFile(join(this.projectDir(), "issues.json"));
91
+ if (issues) {
92
+ return issues.map((issue) => ({
93
+ ...issue,
94
+ completedOnce: issue.completedOnce ?? false
95
+ }));
96
+ }
97
+ const legacyLeases = await readJsonFile(join(this.projectDir(), "leases.json")) ?? [];
98
+ return legacyLeases.map((lease) => ({
99
+ issueId: lease.issueId,
100
+ identifier: lease.issueIdentifier,
101
+ workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(lease.issueIdentifier),
102
+ completedOnce: false,
103
+ state: lease.status === "active" ? "claimed" : "released",
104
+ currentRunId: lease.status === "active" ? lease.runId : null,
105
+ retryEntry: null,
106
+ updatedAt: lease.updatedAt
107
+ }));
108
+ }
109
+ async loadRun(runId) {
110
+ return readJsonFile(join(this.runDir(runId), "run.json"));
111
+ }
112
+ async loadAllRuns() {
113
+ const runIds = await safeReadDir(join(this.projectDir(), "runs"));
114
+ const runs = await mapWithConcurrency(runIds, RUN_RECORD_LOAD_CONCURRENCY, (runId) => this.loadRun(runId));
115
+ return runs.filter((run) => Boolean(run));
116
+ }
117
+ async loadRecentRunEvents(runId, limit = DEFAULT_RECENT_EVENT_LIMIT) {
118
+ if (limit <= 0) {
119
+ return [];
120
+ }
121
+ const path = join(this.runDir(runId), "events.ndjson");
122
+ try {
123
+ const handle = await open(path, "r");
124
+ try {
125
+ const stats = await handle.stat();
126
+ let position = stats.size;
127
+ let bytesScanned = 0;
128
+ let newlineCount = 0;
129
+ const chunks = [];
130
+ while (position > 0 && bytesScanned < MAX_RECENT_EVENT_SCAN_BYTES && newlineCount <= limit) {
131
+ const readSize = Math.min(position, RECENT_EVENT_CHUNK_SIZE, MAX_RECENT_EVENT_SCAN_BYTES - bytesScanned);
132
+ position -= readSize;
133
+ const chunk = Buffer.allocUnsafe(readSize);
134
+ const { bytesRead } = await handle.read(chunk, 0, readSize, position);
135
+ if (bytesRead === 0) {
136
+ break;
137
+ }
138
+ const populatedChunk = chunk.subarray(0, bytesRead);
139
+ chunks.unshift(populatedChunk);
140
+ bytesScanned += bytesRead;
141
+ newlineCount += countNewlines(populatedChunk);
142
+ }
143
+ return parseRecentEvents(Buffer.concat(chunks).toString("utf8"), limit, {
144
+ allowPartialFirstLine: position > 0
145
+ });
146
+ } finally {
147
+ await handle.close();
148
+ }
149
+ } catch (error) {
150
+ if (isFileMissing(error)) {
151
+ return [];
152
+ }
153
+ throw error;
154
+ }
155
+ }
156
+ };
157
+ function countNewlines(chunk) {
158
+ let count = 0;
159
+ for (const byte of chunk) {
160
+ if (byte === 10) {
161
+ count += 1;
162
+ }
163
+ }
164
+ return count;
165
+ }
166
+ async function statusForIssue(reader, issueIdentifier) {
167
+ const issueRecords = await reader.loadProjectIssueOrchestrations();
168
+ const issueRecord = issueRecords.find((record) => record.identifier === issueIdentifier);
169
+ if (!issueRecord) {
170
+ return null;
171
+ }
172
+ const currentRunCandidate = issueRecord.currentRunId ? await reader.loadRun(issueRecord.currentRunId) : null;
173
+ const currentRun = isMatchingIssueRun(currentRunCandidate, reader.projectId, issueRecord.issueId, issueIdentifier) ? currentRunCandidate : await findLatestRunForIssue(reader, issueRecord.issueId, issueIdentifier);
174
+ const recentEvents = currentRun === null ? [] : await reader.loadRecentRunEvents(currentRun.runId);
175
+ const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
176
+ const currentAttempt = currentRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
177
+ return {
178
+ issue_identifier: issueRecord.identifier,
179
+ issue_id: issueRecord.issueId,
180
+ status: currentRun?.status ?? mapIssueOrchestrationStateToStatus(issueRecord.state),
181
+ workspace: {
182
+ path: currentRun?.workingDirectory ?? null
183
+ },
184
+ attempts: {
185
+ restart_count: Math.max(0, currentAttempt - 1),
186
+ current_retry_attempt: currentAttempt
187
+ },
188
+ running: currentRun === null ? null : {
189
+ session_id: currentRun.runtimeSession?.sessionId ?? null,
190
+ turn_count: currentRun.turnCount ?? null,
191
+ state: currentRun.issueState ?? null,
192
+ started_at: currentRun.startedAt ?? null,
193
+ last_event: currentRun.lastEvent ?? null,
194
+ last_message: latestEventMessage,
195
+ last_event_at: currentRun.lastEventAt ?? null,
196
+ tokens: currentRun.tokenUsage ? {
197
+ input_tokens: currentRun.tokenUsage.inputTokens,
198
+ output_tokens: currentRun.tokenUsage.outputTokens,
199
+ total_tokens: currentRun.tokenUsage.totalTokens
200
+ } : null
201
+ },
202
+ retry: currentRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ? {
203
+ due_at: currentRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ?? "",
204
+ kind: currentRun?.retryKind ?? null,
205
+ error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null
206
+ } : null,
207
+ logs: {
208
+ codex_session_logs: currentRun === null ? [] : [
209
+ {
210
+ label: "worker",
211
+ path: join(reader.runDir(currentRun.runId), "worker.log"),
212
+ url: null
213
+ }
214
+ ]
215
+ },
216
+ recent_events: recentEvents,
217
+ last_error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null,
218
+ tracked: {
219
+ issue_orchestration_state: issueRecord.state,
220
+ current_run_id: issueRecord.currentRunId,
221
+ workspace_key: issueRecord.workspaceKey,
222
+ completed_once: issueRecord.completedOnce,
223
+ run_phase: currentRun?.runPhase ?? null,
224
+ execution_phase: currentRun?.executionPhase ?? null
225
+ }
226
+ };
227
+ }
228
+ async function findLatestRunForIssue(reader, issueId, issueIdentifier) {
229
+ const matchingRuns = (await reader.loadAllRuns()).filter((run) => run.issueId === issueId || run.issueIdentifier === issueIdentifier).sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
230
+ return matchingRuns[0] ?? null;
231
+ }
232
+ function assertValidDashboardProjectId(projectId) {
233
+ if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
234
+ throw new Error(`Invalid project ID "${projectId}". Project IDs must not contain path separators or traversal segments.`);
235
+ }
236
+ }
237
+ function assertValidDashboardRunId(runId) {
238
+ if (runId.length === 0 || runId === "." || runId === ".." || runId.includes("/") || runId.includes("\\")) {
239
+ throw new Error(`Invalid run ID "${runId}". Run IDs must not contain path separators or traversal segments.`);
240
+ }
241
+ }
242
+ async function mapWithConcurrency(items, concurrency, mapper) {
243
+ const results = new Array(items.length);
244
+ let nextIndex = 0;
245
+ const worker = async () => {
246
+ while (nextIndex < items.length) {
247
+ const currentIndex = nextIndex;
248
+ nextIndex += 1;
249
+ results[currentIndex] = await mapper(items[currentIndex]);
250
+ }
251
+ };
252
+ const workerCount = Math.min(Math.max(concurrency, 1), items.length);
253
+ await Promise.all(Array.from({ length: workerCount }, () => worker()));
254
+ return results;
255
+ }
256
+
257
+ // ../dashboard/dist/server.js
258
+ import { createServer } from "http";
259
+ async function resolveDashboardResponse(options) {
260
+ const method = options.method ?? "GET";
261
+ if (options.pathname === "/healthz") {
262
+ return {
263
+ status: 200,
264
+ payload: { ok: true }
265
+ };
266
+ }
267
+ if (options.pathname === "/api/v1/state") {
268
+ if (method !== "GET") {
269
+ return {
270
+ status: 405,
271
+ payload: { error: "Method not allowed" }
272
+ };
273
+ }
274
+ const snapshot = await options.reader.loadProjectState();
275
+ if (!snapshot) {
276
+ return {
277
+ status: 404,
278
+ payload: { error: "Project status not found." }
279
+ };
280
+ }
281
+ return {
282
+ status: 200,
283
+ payload: snapshot
284
+ };
285
+ }
286
+ if (options.pathname.startsWith("/api/v1/")) {
287
+ if (method !== "GET") {
288
+ return {
289
+ status: 405,
290
+ payload: { error: "Method not allowed" }
291
+ };
292
+ }
293
+ const rawIdentifier = options.pathname.slice("/api/v1/".length);
294
+ if (!rawIdentifier || rawIdentifier === "state") {
295
+ return {
296
+ status: 404,
297
+ payload: { error: "Not found" }
298
+ };
299
+ }
300
+ let issueIdentifier;
301
+ try {
302
+ issueIdentifier = decodeURIComponent(rawIdentifier);
303
+ } catch {
304
+ return {
305
+ status: 400,
306
+ payload: {
307
+ error: {
308
+ code: "invalid_issue_identifier",
309
+ message: "Issue identifier path segment is not valid URL encoding."
310
+ }
311
+ }
312
+ };
313
+ }
314
+ const issueStatus = await statusForIssue(options.reader, issueIdentifier);
315
+ if (!issueStatus) {
316
+ return {
317
+ status: 404,
318
+ payload: {
319
+ error: {
320
+ code: "issue_not_found",
321
+ message: `Issue "${issueIdentifier}" is unknown to the current filesystem state.`
322
+ }
323
+ }
324
+ };
325
+ }
326
+ return {
327
+ status: 200,
328
+ payload: issueStatus
329
+ };
330
+ }
331
+ return {
332
+ status: 404,
333
+ payload: { error: "Not found" }
334
+ };
335
+ }
336
+
337
+ // src/commands/start.ts
338
+ function timestamp() {
339
+ const now = /* @__PURE__ */ new Date();
340
+ const hh = String(now.getHours()).padStart(2, "0");
341
+ const mm = String(now.getMinutes()).padStart(2, "0");
342
+ const ss = String(now.getSeconds()).padStart(2, "0");
343
+ return dim(`${hh}:${mm}:${ss}`);
344
+ }
345
+ function logLine(icon, msg) {
346
+ process.stdout.write(`${timestamp()} ${icon} ${msg}
347
+ `);
348
+ }
349
+ var DEFAULT_HTTP_PORT = 4680;
350
+ var HTTP_HOST = "0.0.0.0";
351
+ function parseStartArgs(args) {
352
+ const parsed = {
353
+ daemon: false
354
+ };
355
+ for (let i = 0; i < args.length; i += 1) {
356
+ const arg = args[i];
357
+ if (arg === "--daemon" || arg === "-d") {
358
+ parsed.daemon = true;
359
+ continue;
360
+ }
361
+ if (arg === "--http") {
362
+ const value = args[i + 1];
363
+ if (!value || value.startsWith("-")) {
364
+ parsed.httpPort = DEFAULT_HTTP_PORT;
365
+ continue;
366
+ }
367
+ parsed.httpPort = parsePort(value, arg);
368
+ i += 1;
369
+ continue;
370
+ }
371
+ if (arg === "--project" || arg === "--project-id") {
372
+ const value = args[i + 1];
373
+ if (!value || value.startsWith("-")) {
374
+ parsed.error = `Option '${arg}' argument missing`;
375
+ return parsed;
376
+ }
377
+ parsed.projectId = value;
378
+ i += 1;
379
+ continue;
380
+ }
381
+ if (arg === "--log-level") {
382
+ const value = args[i + 1];
383
+ if (!value || value.startsWith("-")) {
384
+ parsed.error = `Option '${arg}' argument missing`;
385
+ return parsed;
386
+ }
387
+ parsed.logLevel = value;
388
+ i += 1;
389
+ continue;
390
+ }
391
+ if (arg?.startsWith("-")) {
392
+ parsed.error = `Unknown option '${arg}'`;
393
+ return parsed;
394
+ }
395
+ }
396
+ return parsed;
397
+ }
398
+ function logTickResult(snapshot, prevSnapshot, isFirst) {
399
+ if (isFirst) {
400
+ const healthColor = snapshot.health === "degraded" ? red : snapshot.health === "running" ? green : cyan;
401
+ logLine(
402
+ green("\u25CF"),
403
+ `Project ${bold(snapshot.slug)} connected ${dim("(")}${healthColor(snapshot.health)}${dim(")")}`
404
+ );
405
+ if (snapshot.summary.activeRuns > 0) {
406
+ logLine(cyan("\u25B8"), `${snapshot.summary.activeRuns} active run(s)`);
407
+ }
408
+ return;
409
+ }
410
+ if (prevSnapshot && prevSnapshot.health !== snapshot.health) {
411
+ const icon = snapshot.health === "degraded" ? red("\u25CF") : green("\u25CF");
412
+ logLine(
413
+ icon,
414
+ `Health changed: ${prevSnapshot.health} \u2192 ${bold(snapshot.health)}`
415
+ );
416
+ }
417
+ if (snapshot.lastError && snapshot.lastError !== prevSnapshot?.lastError) {
418
+ logLine(red("\u2717"), red(snapshot.lastError));
419
+ }
420
+ if (!snapshot.lastError && prevSnapshot?.lastError) {
421
+ logLine(green("\u2713"), green("Error cleared"));
422
+ }
423
+ const prevDispatched = prevSnapshot?.summary.dispatched ?? 0;
424
+ if (snapshot.summary.dispatched > prevDispatched) {
425
+ const delta = snapshot.summary.dispatched - prevDispatched;
426
+ logLine(yellow("\u25B8"), `Dispatched ${bold(String(delta))} new run(s)`);
427
+ }
428
+ const prevRunIds = new Set(
429
+ prevSnapshot?.activeRuns.map((run) => run.runId) ?? []
430
+ );
431
+ for (const run of snapshot.activeRuns) {
432
+ if (!prevRunIds.has(run.runId)) {
433
+ logLine(
434
+ cyan("\u25B8"),
435
+ `Run started: ${bold(run.issueIdentifier)} ${dim("state=")}${run.issueState} ${dim("status=")}${run.status}`
436
+ );
437
+ }
438
+ }
439
+ const currentRunIds = new Set(snapshot.activeRuns.map((run) => run.runId));
440
+ for (const prevRun of prevSnapshot?.activeRuns ?? []) {
441
+ if (!currentRunIds.has(prevRun.runId)) {
442
+ logLine(
443
+ green("\u2713"),
444
+ `Run finished: ${bold(prevRun.issueIdentifier)} ${dim("(")}${prevRun.status}${dim(")")}`
445
+ );
446
+ }
447
+ }
448
+ const prevSuppressed = prevSnapshot?.summary.suppressed ?? 0;
449
+ if (snapshot.summary.suppressed > prevSuppressed) {
450
+ const delta = snapshot.summary.suppressed - prevSuppressed;
451
+ logLine(
452
+ dim("\u25CB"),
453
+ dim(`${delta} issue(s) suppressed (already running or at limit)`)
454
+ );
455
+ }
456
+ const prevRecovered = prevSnapshot?.summary.recovered ?? 0;
457
+ if (snapshot.summary.recovered > prevRecovered) {
458
+ const delta = snapshot.summary.recovered - prevRecovered;
459
+ logLine(
460
+ yellow("\u21BA"),
461
+ `Recovered ${bold(String(delta))} stalled run(s)`
462
+ );
463
+ }
464
+ const prevRetryCount = prevSnapshot?.retryQueue.length ?? 0;
465
+ if (snapshot.retryQueue.length > prevRetryCount) {
466
+ const delta = snapshot.retryQueue.length - prevRetryCount;
467
+ logLine(yellow("\u25CC"), `${delta} run(s) queued for retry`);
468
+ }
469
+ const changed = snapshot.health !== prevSnapshot?.health || snapshot.lastError !== prevSnapshot?.lastError || snapshot.summary.dispatched !== prevSnapshot?.summary.dispatched || snapshot.summary.suppressed !== prevSnapshot?.summary.suppressed || snapshot.summary.recovered !== prevSnapshot?.summary.recovered || snapshot.activeRuns.length !== (prevSnapshot?.activeRuns.length ?? 0) || snapshot.retryQueue.length !== (prevSnapshot?.retryQueue.length ?? 0);
470
+ if (!changed) {
471
+ logLine(
472
+ dim("\xB7"),
473
+ dim(
474
+ `tick \u2014 ${snapshot.summary.activeRuns} active, ${snapshot.health}`
475
+ )
476
+ );
477
+ }
478
+ }
479
+ function parsePort(value, optionName) {
480
+ if (!/^\d+$/.test(value)) {
481
+ throw new Error(`Option '${optionName}' must be an integer port number`);
482
+ }
483
+ const parsed = Number.parseInt(value, 10);
484
+ if (!Number.isSafeInteger(parsed) || parsed < 0 || parsed > 65535) {
485
+ throw new Error(
486
+ `Option '${optionName}' must be a port number between 0 and 65535`
487
+ );
488
+ }
489
+ return parsed;
490
+ }
491
+ function respondJson(response, status, payload) {
492
+ response.writeHead(status, {
493
+ "content-type": "application/json"
494
+ });
495
+ response.end(JSON.stringify(payload));
496
+ }
497
+ function formatBoundUrl(server) {
498
+ const address = server.address();
499
+ if (!address || typeof address === "string") {
500
+ return `http://${HTTP_HOST}`;
501
+ }
502
+ const host = address.address === "::" || address.address === "::1" || address.address === "0.0.0.0" || address.address === "127.0.0.1" ? "localhost" : address.address;
503
+ const urlHost = host.includes(":") ? `[${host}]` : host;
504
+ return `http://${urlHost}:${address.port}`;
505
+ }
506
+ function logHttpRequestError(error) {
507
+ const message = error instanceof Error ? error.stack ?? error.message : String(error);
508
+ process.stderr.write(`[start] HTTP request failed: ${message}
509
+ `);
510
+ }
511
+ async function closeHttpServer(server) {
512
+ if (!server) {
513
+ return;
514
+ }
515
+ await new Promise((resolveClose, rejectClose) => {
516
+ server.close((error) => {
517
+ if (error) {
518
+ rejectClose(error);
519
+ return;
520
+ }
521
+ resolveClose();
522
+ });
523
+ });
524
+ }
525
+ async function writeHttpBindingState(configDir, projectId, binding) {
526
+ await writeJsonFile(httpStatusPath(configDir, projectId), binding);
527
+ }
528
+ async function removeHttpBindingState(configDir, projectId) {
529
+ await rm(httpStatusPath(configDir, projectId), { force: true });
530
+ }
531
+ async function startHttpServer(input) {
532
+ const reader = new DashboardFsReader(input.runtimeRoot, input.projectId);
533
+ for (let port = input.initialPort; port <= 65535; port += 1) {
534
+ const server = createServer2((request, response) => {
535
+ void (async () => {
536
+ try {
537
+ const url = new URL(request.url ?? "/", `http://${HTTP_HOST}`);
538
+ if (request.method === "POST" && url.pathname === "/api/v1/refresh") {
539
+ request.resume();
540
+ input.service.requestReconcile();
541
+ respondJson(response, 202, { ok: true });
542
+ return;
543
+ }
544
+ const resolved = await resolveDashboardResponse({
545
+ pathname: url.pathname,
546
+ method: request.method ?? "GET",
547
+ reader
548
+ });
549
+ respondJson(response, resolved.status, resolved.payload);
550
+ } catch (error) {
551
+ logHttpRequestError(error);
552
+ if (!response.headersSent) {
553
+ respondJson(response, 500, {
554
+ error: "Internal server error"
555
+ });
556
+ } else {
557
+ response.end();
558
+ }
559
+ }
560
+ })();
561
+ });
562
+ try {
563
+ await new Promise((resolveReady, rejectReady) => {
564
+ const handleListening = () => {
565
+ cleanup();
566
+ resolveReady();
567
+ };
568
+ const handleError = (error) => {
569
+ cleanup();
570
+ rejectReady(error);
571
+ };
572
+ const cleanup = () => {
573
+ server.off("listening", handleListening);
574
+ server.off("error", handleError);
575
+ };
576
+ server.once("listening", handleListening);
577
+ server.once("error", handleError);
578
+ server.listen(port, HTTP_HOST);
579
+ });
580
+ return {
581
+ server,
582
+ port,
583
+ url: formatBoundUrl(server)
584
+ };
585
+ } catch (error) {
586
+ await closeHttpServer(server).catch(() => {
587
+ });
588
+ if (error?.code === "EADDRINUSE") {
589
+ continue;
590
+ }
591
+ throw error;
592
+ }
593
+ }
594
+ throw new Error(
595
+ `Unable to bind HTTP server starting from port ${input.initialPort}`
596
+ );
597
+ }
598
+ var handler = async (args, options) => {
599
+ setNoColor(options.noColor);
600
+ let parsed;
601
+ try {
602
+ parsed = parseStartArgs(args);
603
+ } catch (error) {
604
+ process.stderr.write(
605
+ `${error instanceof Error ? error.message : "Invalid arguments"}
606
+ `
607
+ );
608
+ process.exitCode = 2;
609
+ return;
610
+ }
611
+ if (parsed.error) {
612
+ process.stderr.write(`${parsed.error}
613
+ `);
614
+ process.stderr.write(
615
+ "Usage: gh-symphony start --project-id <project-id> [--daemon] [--http [port]]\n"
616
+ );
617
+ process.exitCode = 2;
618
+ return;
619
+ }
620
+ const projectConfig = await resolveManagedProjectConfig({
621
+ configDir: options.configDir,
622
+ requestedProjectId: parsed.projectId
623
+ });
624
+ if (!projectConfig) {
625
+ handleMissingManagedProjectConfig();
626
+ return;
627
+ }
628
+ const runtimeRoot = resolveRuntimeRoot(options.configDir);
629
+ const projectId = projectConfig.projectId;
630
+ let logLevel;
631
+ try {
632
+ logLevel = resolveOrchestratorLogLevel(
633
+ parsed.logLevel ?? process.env.SYMPHONY_LOG_LEVEL
634
+ );
635
+ } catch (error) {
636
+ process.stderr.write(
637
+ `${error instanceof Error ? error.message : "Unsupported log level"}
638
+ `
639
+ );
640
+ process.exitCode = 2;
641
+ return;
642
+ }
643
+ if (parsed.daemon) {
644
+ await startDaemon(options, projectId, parsed.logLevel, parsed.httpPort);
645
+ return;
646
+ }
647
+ if (!process.env.GITHUB_GRAPHQL_TOKEN) {
648
+ try {
649
+ process.env.GITHUB_GRAPHQL_TOKEN = getGhToken();
650
+ } catch {
651
+ }
652
+ }
653
+ let projectLock = null;
654
+ try {
655
+ projectLock = await acquireProjectLock({
656
+ runtimeRoot,
657
+ projectId
658
+ });
659
+ await removeHttpBindingState(options.configDir, projectId);
660
+ const store = createStore(runtimeRoot);
661
+ let prevSnapshot = null;
662
+ let isFirst = true;
663
+ const service = new OrchestratorService(store, projectConfig, {
664
+ logLevel,
665
+ onTick: async (snapshot) => {
666
+ try {
667
+ logTickResult(snapshot, prevSnapshot, isFirst);
668
+ if (!isFirst) {
669
+ const currentRunIds = new Set(
670
+ snapshot.activeRuns.map((run) => run.runId)
671
+ );
672
+ for (const prevRun of prevSnapshot?.activeRuns ?? []) {
673
+ if (!currentRunIds.has(prevRun.runId)) {
674
+ await tailWorkerLog(
675
+ runtimeRoot,
676
+ projectId,
677
+ prevRun.runId,
678
+ prevRun.issueIdentifier
679
+ );
680
+ }
681
+ }
682
+ }
683
+ prevSnapshot = snapshot;
684
+ isFirst = false;
685
+ } catch (error) {
686
+ logLine(
687
+ red("\u2717"),
688
+ red(
689
+ `Tick error: ${error instanceof Error ? error.message : "Unknown error"}`
690
+ )
691
+ );
692
+ }
693
+ }
694
+ });
695
+ const httpServer = parsed.httpPort !== void 0 ? await startHttpServer({
696
+ runtimeRoot,
697
+ projectId,
698
+ initialPort: parsed.httpPort,
699
+ service
700
+ }) : null;
701
+ if (httpServer) {
702
+ try {
703
+ await writeHttpBindingState(options.configDir, projectId, {
704
+ host: HTTP_HOST,
705
+ port: httpServer.port,
706
+ endpoint: httpServer.url
707
+ });
708
+ } catch (error) {
709
+ logLine(
710
+ yellow("\u26A0"),
711
+ yellow(
712
+ `Failed to persist HTTP binding state (http.json): ${error instanceof Error ? error.message : "Unknown error"}`
713
+ )
714
+ );
715
+ }
716
+ }
717
+ logLine(
718
+ green("\u25B2"),
719
+ `Starting orchestrator for project: ${bold(projectId)}`
720
+ );
721
+ if (httpServer) {
722
+ logLine(
723
+ cyan("\u25A1"),
724
+ `HTTP dashboard listening on ${httpServer.url}`
725
+ );
726
+ }
727
+ logLine(dim("\xB7"), dim("Press Ctrl+C to stop"));
728
+ let shuttingDown = false;
729
+ let shutdownPromise = null;
730
+ const shutdown = async () => {
731
+ if (shuttingDown) {
732
+ return shutdownPromise;
733
+ }
734
+ shuttingDown = true;
735
+ const heldLock = projectLock;
736
+ projectLock = null;
737
+ shutdownPromise = shutdownForegroundOrchestrator({
738
+ configDir: options.configDir,
739
+ projectId,
740
+ httpServer: httpServer?.server,
741
+ projectLock: heldLock,
742
+ service
743
+ });
744
+ return shutdownPromise;
745
+ };
746
+ process.on("SIGINT", () => {
747
+ void shutdown();
748
+ });
749
+ process.on("SIGTERM", () => {
750
+ void shutdown();
751
+ });
752
+ try {
753
+ while (!shuttingDown) {
754
+ try {
755
+ await service.run();
756
+ break;
757
+ } catch (error) {
758
+ if (shuttingDown) {
759
+ break;
760
+ }
761
+ logLine(
762
+ red("\u2717"),
763
+ red(
764
+ `Run loop error: ${error instanceof Error ? error.message : "Unknown error"}`
765
+ )
766
+ );
767
+ }
768
+ }
769
+ } finally {
770
+ if (shutdownPromise) {
771
+ await shutdownPromise;
772
+ }
773
+ }
774
+ } finally {
775
+ await releaseProjectLock(projectLock);
776
+ }
777
+ };
778
+ async function shutdownForegroundOrchestrator(input) {
779
+ logLine(yellow("\u25BC"), "Shutting down...");
780
+ if (input.service) {
781
+ try {
782
+ await input.service.shutdown();
783
+ } catch (error) {
784
+ logLine(
785
+ red("\u2717"),
786
+ red(
787
+ `Failed to shut down workers: ${error instanceof Error ? error.message : "Unknown error"}`
788
+ )
789
+ );
790
+ }
791
+ }
792
+ try {
793
+ await closeHttpServer(input.httpServer);
794
+ } catch (error) {
795
+ logLine(
796
+ yellow("\u26A0"),
797
+ `Failed to stop HTTP server: ${error instanceof Error ? error.message : "Unknown error"}`
798
+ );
799
+ }
800
+ try {
801
+ await removeHttpBindingState(input.configDir, input.projectId);
802
+ } catch (error) {
803
+ logLine(
804
+ yellow("\u26A0"),
805
+ `Failed to remove HTTP state: ${error instanceof Error ? error.message : "Unknown error"}`
806
+ );
807
+ }
808
+ try {
809
+ await (input.releaseLock ?? releaseProjectLock)(input.projectLock);
810
+ } catch (error) {
811
+ logLine(
812
+ yellow("\u26A0"),
813
+ `Failed to release project lock: ${error instanceof Error ? error.message : "Unknown error"}`
814
+ );
815
+ }
816
+ return (input.exit ?? process.exit)(0);
817
+ }
818
+ async function tailWorkerLog(runtimeRoot, projectId, runId, issueIdentifier) {
819
+ try {
820
+ const logPath = join2(runtimeRoot, "projects", projectId, "runs", runId, "worker.log");
821
+ const content = await readFile(logPath, "utf8");
822
+ const lines = content.split("\n").filter((l) => l.trim());
823
+ if (lines.length === 0) return;
824
+ const tail = lines.slice(-30);
825
+ logLine(red("\u2717"), red(`Worker stderr (${issueIdentifier}):`));
826
+ for (const line of tail) {
827
+ process.stdout.write(` ${dim(line)}
828
+ `);
829
+ }
830
+ } catch {
831
+ }
832
+ }
833
+ var start_default = handler;
834
+ async function startDaemon(options, projectId, logLevel, httpPort) {
835
+ const logPath = orchestratorLogPath(options.configDir, projectId);
836
+ await mkdir(dirname(logPath), { recursive: true });
837
+ const { openSync } = await import("fs");
838
+ const logFd = openSync(logPath, "a");
839
+ const child = spawn(
840
+ process.execPath,
841
+ [
842
+ process.argv[1],
843
+ "start",
844
+ "--project",
845
+ projectId,
846
+ ...httpPort !== void 0 ? ["--http", String(httpPort)] : [],
847
+ ...logLevel ? ["--log-level", logLevel] : []
848
+ ],
849
+ {
850
+ cwd: process.cwd(),
851
+ env: {
852
+ ...process.env,
853
+ GH_SYMPHONY_CONFIG_DIR: options.configDir
854
+ },
855
+ detached: true,
856
+ stdio: ["ignore", logFd, logFd]
857
+ }
858
+ );
859
+ const pidPath = daemonPidPath(options.configDir, projectId);
860
+ await mkdir(dirname(pidPath), { recursive: true });
861
+ await writeFile(pidPath, String(child.pid), "utf8");
862
+ child.unref();
863
+ const { closeSync } = await import("fs");
864
+ closeSync(logFd);
865
+ process.stdout.write(
866
+ `Orchestrator started in background (PID: ${child.pid}).
867
+ Logs: ${logPath}
868
+ Stop with: gh-symphony project stop --project-id ${projectId}
869
+ `
870
+ );
871
+ }
872
+
873
+ export {
874
+ shutdownForegroundOrchestrator,
875
+ start_default
876
+ };