@gh-symphony/cli 0.0.17 → 0.0.19

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 (32) hide show
  1. package/README.md +105 -9
  2. package/dist/{chunk-EFMFGOWM.js → chunk-6CI3UUMH.js} +282 -57
  3. package/dist/chunk-C7G7RJ4G.js +146 -0
  4. package/dist/{chunk-MHIWAIVD.js → chunk-GKENCODJ.js} +141 -53
  5. package/dist/{project-557FE2GD.js → chunk-H2YXSYOZ.js} +108 -92
  6. package/dist/{chunk-TF3QNWNC.js → chunk-M3IFVLQS.js} +246 -212
  7. package/dist/{chunk-IWR4UQEJ.js → chunk-RN2PACNV.js} +350 -523
  8. package/dist/chunk-TILHWBP6.js +638 -0
  9. package/dist/{chunk-6HBZC3BE.js → chunk-XN5ABWZ6.js} +23 -5
  10. package/dist/{chunk-76QPITKI.js → chunk-Y6TYJMNT.js} +1 -1
  11. package/dist/{config-cmd-AZ7POMAA.js → config-cmd-DNXNL26Z.js} +3 -1
  12. package/dist/doctor-IYHCFXOZ.js +1126 -0
  13. package/dist/index.js +157 -19
  14. package/dist/init-KZT6YNOH.js +33 -0
  15. package/dist/{logs-6LNGT2GF.js → logs-6JKKYDGJ.js} +1 -1
  16. package/dist/project-DNALEWO3.js +22 -0
  17. package/dist/{recover-LVBI2TGH.js → recover-C3V2QAUB.js} +3 -3
  18. package/dist/repo-HDDE7OUI.js +321 -0
  19. package/dist/{run-WITYAYFZ.js → run-XI2S5Y4V.js} +3 -3
  20. package/dist/setup-K4CYYJBF.js +431 -0
  21. package/dist/{start-JUFKNL3N.js → start-M6IQGRFO.js} +5 -5
  22. package/dist/{status-3WK5BWRZ.js → status-QSCFVGRQ.js} +2 -2
  23. package/dist/{stop-AA3AP5M6.js → stop-7MFCBQVW.js} +2 -2
  24. package/dist/upgrade-F4VE4XBS.js +165 -0
  25. package/dist/{version-YVM2A25J.js → version-Y5RYNWMF.js} +1 -1
  26. package/dist/worker-entry.js +39 -11
  27. package/dist/workflow-TBIFY5MO.js +497 -0
  28. package/package.json +4 -4
  29. package/dist/chunk-JO3AXHQI.js +0 -130
  30. package/dist/chunk-TH5QPO3Y.js +0 -67
  31. package/dist/init-EZXQAXZM.js +0 -17
  32. package/dist/repo-R3XBIVAX.js +0 -121
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadGlobalConfig,
4
+ loadProjectConfig
5
+ } from "./chunk-ROGRTUFI.js";
6
+
7
+ // src/project-selection.ts
8
+ import * as p from "@clack/prompts";
9
+ function isInteractiveTerminal() {
10
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
11
+ }
12
+ function explicitProjectRequiredMessage() {
13
+ return "Multiple projects are configured. Re-run with --project-id in non-interactive environments.\n";
14
+ }
15
+ async function inspectManagedProjectSelection(input) {
16
+ if (input.requestedProjectId) {
17
+ const projectConfig = await loadProjectConfig(
18
+ input.configDir,
19
+ input.requestedProjectId
20
+ );
21
+ if (!projectConfig) {
22
+ return {
23
+ kind: "requested_project_missing",
24
+ projectId: input.requestedProjectId,
25
+ message: `Project "${input.requestedProjectId}" is not configured. Run 'gh-symphony project add' or choose an existing project.`
26
+ };
27
+ }
28
+ return {
29
+ kind: "resolved",
30
+ projectId: input.requestedProjectId,
31
+ projectConfig
32
+ };
33
+ }
34
+ const global = await loadGlobalConfig(input.configDir);
35
+ if (!global) {
36
+ return {
37
+ kind: "missing_global_config",
38
+ message: "No CLI configuration found. Run 'gh-symphony project add' first."
39
+ };
40
+ }
41
+ const projectIds = global.projects ?? [];
42
+ if (projectIds.length === 0) {
43
+ return {
44
+ kind: "no_projects",
45
+ message: "No managed projects are configured. Run 'gh-symphony project add' first."
46
+ };
47
+ }
48
+ if (projectIds.length > 1 && !isInteractiveTerminal()) {
49
+ return {
50
+ kind: "multiple_projects_require_selection",
51
+ message: explicitProjectRequiredMessage().trimEnd()
52
+ };
53
+ }
54
+ if (global.activeProject) {
55
+ const projectConfig = await loadProjectConfig(
56
+ input.configDir,
57
+ global.activeProject
58
+ );
59
+ if (!projectConfig) {
60
+ return {
61
+ kind: "active_project_missing",
62
+ projectId: global.activeProject,
63
+ message: `Active project "${global.activeProject}" is configured in config.json but its project config is missing. Re-run 'gh-symphony project add' or 'gh-symphony project switch'.`
64
+ };
65
+ }
66
+ return {
67
+ kind: "resolved",
68
+ projectId: global.activeProject,
69
+ projectConfig
70
+ };
71
+ }
72
+ if (projectIds.length === 1) {
73
+ const projectId = projectIds[0];
74
+ const projectConfig = await loadProjectConfig(input.configDir, projectId);
75
+ if (!projectConfig) {
76
+ return {
77
+ kind: "configured_project_missing",
78
+ projectId,
79
+ message: `Configured project "${projectId}" is missing its project config file. Re-run 'gh-symphony project add'.`
80
+ };
81
+ }
82
+ return {
83
+ kind: "resolved",
84
+ projectId,
85
+ projectConfig
86
+ };
87
+ }
88
+ return {
89
+ kind: "multiple_projects_require_selection",
90
+ message: "Multiple projects are configured and no active project is set. Run 'gh-symphony project switch' or re-run with --project-id."
91
+ };
92
+ }
93
+ async function resolveManagedProjectConfig(input) {
94
+ if (input.requestedProjectId) {
95
+ return loadProjectConfig(input.configDir, input.requestedProjectId);
96
+ }
97
+ const global = await loadGlobalConfig(input.configDir);
98
+ const projectIds = global?.projects ?? [];
99
+ if (projectIds.length === 0) {
100
+ return null;
101
+ }
102
+ if (projectIds.length === 1) {
103
+ return loadProjectConfig(input.configDir, projectIds[0]);
104
+ }
105
+ if (!isInteractiveTerminal()) {
106
+ process.stderr.write(explicitProjectRequiredMessage());
107
+ process.exitCode = 1;
108
+ return null;
109
+ }
110
+ const projects = await Promise.all(
111
+ projectIds.map(async (projectId) => ({
112
+ projectId,
113
+ config: await loadProjectConfig(input.configDir, projectId)
114
+ }))
115
+ );
116
+ const selected = await p.select({
117
+ message: "Select a project:",
118
+ options: projects.map(({ projectId, config }) => ({
119
+ value: projectId,
120
+ label: config?.displayName ?? config?.slug ?? projectId,
121
+ hint: projectId === global?.activeProject ? "current" : config && config.displayName && config.displayName !== projectId ? projectId : void 0
122
+ })),
123
+ maxItems: 10
124
+ });
125
+ if (p.isCancel(selected)) {
126
+ p.cancel("Cancelled.");
127
+ process.exitCode = 130;
128
+ return null;
129
+ }
130
+ return loadProjectConfig(input.configDir, selected);
131
+ }
132
+ function handleMissingManagedProjectConfig() {
133
+ if (process.exitCode) {
134
+ return;
135
+ }
136
+ process.stderr.write(
137
+ "No project configured. Run 'gh-symphony project add' first.\n"
138
+ );
139
+ process.exitCode = 1;
140
+ }
141
+
142
+ export {
143
+ inspectManagedProjectSelection,
144
+ resolveManagedProjectConfig,
145
+ handleMissingManagedProjectConfig
146
+ };
@@ -1,23 +1,11 @@
1
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
2
  import {
15
3
  OrchestratorService,
16
4
  acquireProjectLock,
17
5
  createStore,
18
6
  releaseProjectLock,
19
7
  resolveOrchestratorLogLevel
20
- } from "./chunk-EFMFGOWM.js";
8
+ } from "./chunk-6CI3UUMH.js";
21
9
  import {
22
10
  deriveIssueWorkspaceKeyFromIdentifier,
23
11
  isFileMissing,
@@ -26,14 +14,26 @@ import {
26
14
  parseRecentEvents,
27
15
  readJsonFile,
28
16
  safeReadDir
29
- } from "./chunk-TF3QNWNC.js";
17
+ } from "./chunk-M3IFVLQS.js";
18
+ import {
19
+ getGhToken
20
+ } from "./chunk-TILHWBP6.js";
21
+ import {
22
+ bold,
23
+ cyan,
24
+ dim,
25
+ green,
26
+ red,
27
+ setNoColor,
28
+ yellow
29
+ } from "./chunk-MVRF7BES.js";
30
30
  import {
31
31
  resolveRuntimeRoot
32
32
  } from "./chunk-5NV3LSAJ.js";
33
33
  import {
34
34
  handleMissingManagedProjectConfig,
35
35
  resolveManagedProjectConfig
36
- } from "./chunk-TH5QPO3Y.js";
36
+ } from "./chunk-C7G7RJ4G.js";
37
37
  import {
38
38
  daemonPidPath,
39
39
  httpStatusPath,
@@ -91,7 +91,8 @@ var DashboardFsReader = class {
91
91
  if (issues) {
92
92
  return issues.map((issue) => ({
93
93
  ...issue,
94
- completedOnce: issue.completedOnce ?? false
94
+ completedOnce: issue.completedOnce ?? false,
95
+ failureRetryCount: issue.failureRetryCount ?? 0
95
96
  }));
96
97
  }
97
98
  const legacyLeases = await readJsonFile(join(this.projectDir(), "leases.json")) ?? [];
@@ -100,6 +101,7 @@ var DashboardFsReader = class {
100
101
  identifier: lease.issueIdentifier,
101
102
  workspaceKey: deriveIssueWorkspaceKeyFromIdentifier(lease.issueIdentifier),
102
103
  completedOnce: false,
104
+ failureRetryCount: 0,
103
105
  state: lease.status === "active" ? "claimed" : "released",
104
106
  currentRunId: lease.status === "active" ? lease.runId : null,
105
107
  retryEntry: null,
@@ -114,6 +116,24 @@ var DashboardFsReader = class {
114
116
  const runs = await mapWithConcurrency(runIds, RUN_RECORD_LOAD_CONCURRENCY, (runId) => this.loadRun(runId));
115
117
  return runs.filter((run) => Boolean(run));
116
118
  }
119
+ async loadRunsForIssue(issueId, issueIdentifier) {
120
+ const runIds = await safeReadDir(join(this.projectDir(), "runs"));
121
+ const runs = await mapWithConcurrency(runIds, RUN_RECORD_LOAD_CONCURRENCY, async (runId) => {
122
+ try {
123
+ const run = await this.loadRun(runId);
124
+ if (!run) {
125
+ return null;
126
+ }
127
+ return run.issueId === issueId || run.issueIdentifier === issueIdentifier ? run : null;
128
+ } catch (error) {
129
+ if (isFileMissing(error)) {
130
+ return null;
131
+ }
132
+ return null;
133
+ }
134
+ });
135
+ return runs.filter((run) => Boolean(run));
136
+ }
117
137
  async loadRecentRunEvents(runId, limit = DEFAULT_RECENT_EVENT_LIMIT) {
118
138
  if (limit <= 0) {
119
139
  return [];
@@ -170,64 +190,81 @@ async function statusForIssue(reader, issueIdentifier) {
170
190
  return null;
171
191
  }
172
192
  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);
193
+ const currentRun = isMatchingIssueRun(currentRunCandidate, reader.projectId, issueRecord.issueId, issueIdentifier) ? currentRunCandidate : null;
194
+ const issueRuns = currentRun === null ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : currentRun.tokenUsage ? await reader.loadRunsForIssue(issueRecord.issueId, issueIdentifier) : null;
195
+ const resolvedRun = currentRun ?? findLatestRunForIssue(issueRuns ?? []);
196
+ const recentEvents = resolvedRun === null ? [] : await reader.loadRecentRunEvents(resolvedRun.runId);
197
+ const cumulativeTokens = aggregateIssueTokenUsage(issueRuns ?? []);
175
198
  const latestEventMessage = recentEvents[recentEvents.length - 1]?.message ?? null;
176
- const currentAttempt = currentRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
199
+ const currentAttempt = resolvedRun?.attempt ?? issueRecord.retryEntry?.attempt ?? 0;
177
200
  return {
178
201
  issue_identifier: issueRecord.identifier,
179
202
  issue_id: issueRecord.issueId,
180
- status: currentRun?.status ?? mapIssueOrchestrationStateToStatus(issueRecord.state),
203
+ status: resolvedRun?.status ?? mapIssueOrchestrationStateToStatus(issueRecord.state),
181
204
  workspace: {
182
- path: currentRun?.workingDirectory ?? null
205
+ path: resolvedRun?.workingDirectory ?? null
183
206
  },
184
207
  attempts: {
185
208
  restart_count: Math.max(0, currentAttempt - 1),
186
209
  current_retry_attempt: currentAttempt
187
210
  },
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,
211
+ running: resolvedRun === null ? null : {
212
+ session_id: resolvedRun.runtimeSession?.sessionId ?? null,
213
+ turn_count: resolvedRun.turnCount ?? null,
214
+ state: resolvedRun.issueState ?? null,
215
+ started_at: resolvedRun.startedAt ?? null,
216
+ last_event: resolvedRun.lastEvent ?? null,
194
217
  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
218
+ last_event_at: resolvedRun.lastEventAt ?? null,
219
+ tokens: resolvedRun.tokenUsage ? {
220
+ input_tokens: resolvedRun.tokenUsage.inputTokens,
221
+ output_tokens: resolvedRun.tokenUsage.outputTokens,
222
+ total_tokens: resolvedRun.tokenUsage.totalTokens,
223
+ cumulative_input_tokens: cumulativeTokens.inputTokens,
224
+ cumulative_output_tokens: cumulativeTokens.outputTokens,
225
+ cumulative_total_tokens: cumulativeTokens.totalTokens
200
226
  } : null
201
227
  },
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
228
+ retry: resolvedRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ? {
229
+ due_at: resolvedRun?.nextRetryAt ?? issueRecord.retryEntry?.dueAt ?? "",
230
+ kind: resolvedRun?.retryKind ?? null,
231
+ error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null
206
232
  } : null,
207
233
  logs: {
208
- codex_session_logs: currentRun === null ? [] : [
234
+ codex_session_logs: resolvedRun === null ? [] : [
209
235
  {
210
236
  label: "worker",
211
- path: join(reader.runDir(currentRun.runId), "worker.log"),
237
+ path: join(reader.runDir(resolvedRun.runId), "worker.log"),
212
238
  url: null
213
239
  }
214
240
  ]
215
241
  },
216
242
  recent_events: recentEvents,
217
- last_error: currentRun?.lastError ?? issueRecord.retryEntry?.error ?? null,
243
+ last_error: resolvedRun?.lastError ?? issueRecord.retryEntry?.error ?? null,
218
244
  tracked: {
219
245
  issue_orchestration_state: issueRecord.state,
220
246
  current_run_id: issueRecord.currentRunId,
221
247
  workspace_key: issueRecord.workspaceKey,
222
248
  completed_once: issueRecord.completedOnce,
223
- run_phase: currentRun?.runPhase ?? null,
224
- execution_phase: currentRun?.executionPhase ?? null
249
+ run_phase: resolvedRun?.runPhase ?? null,
250
+ execution_phase: resolvedRun?.executionPhase ?? null
225
251
  }
226
252
  };
227
253
  }
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;
254
+ function aggregateIssueTokenUsage(runs) {
255
+ return runs.reduce((total, run) => ({
256
+ inputTokens: total.inputTokens + (run.tokenUsage?.inputTokens ?? 0),
257
+ outputTokens: total.outputTokens + (run.tokenUsage?.outputTokens ?? 0),
258
+ totalTokens: total.totalTokens + (run.tokenUsage?.totalTokens ?? 0)
259
+ }), {
260
+ inputTokens: 0,
261
+ outputTokens: 0,
262
+ totalTokens: 0
263
+ });
264
+ }
265
+ function findLatestRunForIssue(matchingRuns) {
266
+ const sortedRuns = [...matchingRuns].sort((left, right) => new Date(right.updatedAt).getTime() - new Date(left.updatedAt).getTime());
267
+ return sortedRuns[0] ?? null;
231
268
  }
232
269
  function assertValidDashboardProjectId(projectId) {
233
270
  if (projectId.length === 0 || projectId === "." || projectId === ".." || projectId.includes("/") || projectId.includes("\\")) {
@@ -350,7 +387,8 @@ var DEFAULT_HTTP_PORT = 4680;
350
387
  var HTTP_HOST = "0.0.0.0";
351
388
  function parseStartArgs(args) {
352
389
  const parsed = {
353
- daemon: false
390
+ daemon: false,
391
+ once: false
354
392
  };
355
393
  for (let i = 0; i < args.length; i += 1) {
356
394
  const arg = args[i];
@@ -358,6 +396,10 @@ function parseStartArgs(args) {
358
396
  parsed.daemon = true;
359
397
  continue;
360
398
  }
399
+ if (arg === "--once") {
400
+ parsed.once = true;
401
+ continue;
402
+ }
361
403
  if (arg === "--http") {
362
404
  const value = args[i + 1];
363
405
  if (!value || value.startsWith("-")) {
@@ -612,11 +654,16 @@ var handler = async (args, options) => {
612
654
  process.stderr.write(`${parsed.error}
613
655
  `);
614
656
  process.stderr.write(
615
- "Usage: gh-symphony start --project-id <project-id> [--daemon] [--http [port]]\n"
657
+ "Usage: gh-symphony start --project-id <project-id> [--daemon] [--once] [--http [port]]\n"
616
658
  );
617
659
  process.exitCode = 2;
618
660
  return;
619
661
  }
662
+ if (parsed.daemon && parsed.once) {
663
+ process.stderr.write("Options '--daemon' and '--once' cannot be used together\n");
664
+ process.exitCode = 2;
665
+ return;
666
+ }
620
667
  const projectConfig = await resolveManagedProjectConfig({
621
668
  configDir: options.configDir,
622
669
  requestedProjectId: parsed.projectId
@@ -724,14 +771,20 @@ var handler = async (args, options) => {
724
771
  `HTTP dashboard listening on ${httpServer.url}`
725
772
  );
726
773
  }
727
- logLine(dim("\xB7"), dim("Press Ctrl+C to stop"));
774
+ logLine(
775
+ dim("\xB7"),
776
+ dim(parsed.once ? "Running one orchestration tick" : "Press Ctrl+C to stop")
777
+ );
728
778
  let shuttingDown = false;
729
779
  let shutdownPromise = null;
780
+ let keepHttpAliveResolve = null;
730
781
  const shutdown = async () => {
731
782
  if (shuttingDown) {
732
783
  return shutdownPromise;
733
784
  }
734
785
  shuttingDown = true;
786
+ keepHttpAliveResolve?.();
787
+ keepHttpAliveResolve = null;
735
788
  const heldLock = projectLock;
736
789
  projectLock = null;
737
790
  shutdownPromise = shutdownForegroundOrchestrator({
@@ -743,16 +796,31 @@ var handler = async (args, options) => {
743
796
  });
744
797
  return shutdownPromise;
745
798
  };
746
- process.on("SIGINT", () => {
799
+ const handleSigint = () => {
747
800
  void shutdown();
748
- });
749
- process.on("SIGTERM", () => {
801
+ };
802
+ const handleSigterm = () => {
750
803
  void shutdown();
751
- });
804
+ };
805
+ process.on("SIGINT", handleSigint);
806
+ process.on("SIGTERM", handleSigterm);
752
807
  try {
753
808
  while (!shuttingDown) {
754
809
  try {
755
- await service.run();
810
+ await service.run({ once: parsed.once });
811
+ if (parsed.once) {
812
+ if (httpServer) {
813
+ logLine(
814
+ cyan("\u25A1"),
815
+ "One-shot tick completed; HTTP dashboard remains available until Ctrl+C"
816
+ );
817
+ await new Promise((resolve2) => {
818
+ keepHttpAliveResolve = resolve2;
819
+ });
820
+ } else {
821
+ await shutdown();
822
+ }
823
+ }
756
824
  break;
757
825
  } catch (error) {
758
826
  if (shuttingDown) {
@@ -761,12 +829,32 @@ var handler = async (args, options) => {
761
829
  logLine(
762
830
  red("\u2717"),
763
831
  red(
764
- `Run loop error: ${error instanceof Error ? error.message : "Unknown error"}`
832
+ `${parsed.once ? "One-shot run failed" : "Run loop error"}: ${error instanceof Error ? error.message : "Unknown error"}`
765
833
  )
766
834
  );
835
+ if (parsed.once) {
836
+ process.exitCode = 1;
837
+ await closeHttpServer(httpServer?.server).catch((closeError) => {
838
+ logLine(
839
+ yellow("\u26A0"),
840
+ `Failed to stop HTTP server: ${closeError instanceof Error ? closeError.message : "Unknown error"}`
841
+ );
842
+ });
843
+ await removeHttpBindingState(options.configDir, projectId).catch(
844
+ (removeError) => {
845
+ logLine(
846
+ yellow("\u26A0"),
847
+ `Failed to remove HTTP state: ${removeError instanceof Error ? removeError.message : "Unknown error"}`
848
+ );
849
+ }
850
+ );
851
+ return;
852
+ }
767
853
  }
768
854
  }
769
855
  } finally {
856
+ process.off("SIGINT", handleSigint);
857
+ process.off("SIGTERM", handleSigterm);
770
858
  if (shutdownPromise) {
771
859
  await shutdownPromise;
772
860
  }