@gh-symphony/cli 0.0.18 → 0.0.20

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.
package/README.md CHANGED
@@ -8,10 +8,15 @@ The following tools must be installed before using the CLI:
8
8
 
9
9
  - **[Node.js](https://nodejs.org/)** (v24+) with npm
10
10
  - **[Git](https://git-scm.com/)**
11
- - **[GitHub CLI (`gh`)](https://cli.github.com/)** — authenticated with required scopes:
12
- ```bash
13
- gh auth login --scopes repo,read:org,project
14
- ```
11
+ - One GitHub auth source with required scopes (`repo`, `read:org`, `project`):
12
+ - **[GitHub CLI (`gh`)](https://cli.github.com/)**:
13
+ ```bash
14
+ gh auth login --scopes repo,read:org,project
15
+ ```
16
+ - Or `GITHUB_GRAPHQL_TOKEN` for CI or minimal shells:
17
+ ```bash
18
+ export GITHUB_GRAPHQL_TOKEN=ghp_your_classic_token
19
+ ```
15
20
 
16
21
  ## 1. Install Package
17
22
 
@@ -29,7 +34,9 @@ Validate the machine and repo prerequisites before first use:
29
34
 
30
35
  ```bash
31
36
  gh-symphony doctor
37
+ gh-symphony doctor --fix
32
38
  gh-symphony doctor --json
39
+ GITHUB_GRAPHQL_TOKEN=ghp_your_classic_token gh-symphony doctor --json
33
40
  ```
34
41
 
35
42
  Enable shell completion:
@@ -51,19 +58,34 @@ autoload -Uz compinit && compinit
51
58
  Navigate to the repository you want to orchestrate, then run:
52
59
 
53
60
  ```bash
54
- gh-symphony init
61
+ gh-symphony workflow init
62
+ gh-symphony workflow init --dry-run
63
+ gh-symphony workflow validate
64
+ gh-symphony workflow preview
55
65
  ```
56
66
 
57
67
  The interactive wizard will:
58
68
 
59
- 1. Authenticate via `gh` CLI
69
+ 1. Authenticate via `GITHUB_GRAPHQL_TOKEN` or fall back to `gh` CLI
60
70
  2. Let you select a **GitHub Project** to bind
61
71
  3. Map project status columns to workflow phases (active / wait / terminal)
62
72
  4. Generate `WORKFLOW.md` and supporting files in the repository
63
73
 
74
+ Token-only interactive setup is supported:
75
+
76
+ ```bash
77
+ export GITHUB_GRAPHQL_TOKEN=ghp_your_classic_token
78
+ gh-symphony workflow init
79
+ ```
80
+
81
+ Use `--dry-run` to preview the generated write plan first. The preview reports
82
+ whether `WORKFLOW.md`, `.gh-symphony/context.yaml`,
83
+ `.gh-symphony/reference-workflow.md`, and runtime skill files would be created,
84
+ updated, or left unchanged, and then exits without modifying the repository.
85
+
64
86
  ### Customizing Agent Behavior
65
87
 
66
- `gh-symphony init` generates skill files under `.codex/skills/` (or `.claude/skills/` for Claude Code). These skills define how the AI agent handles commits, pushes, pulls, and project status transitions.
88
+ `gh-symphony workflow init` generates skill files under `.codex/skills/` (or `.claude/skills/` for Claude Code). These skills define how the AI agent handles commits, pushes, pulls, and project status transitions.
67
89
 
68
90
  You can further customize the agent's behavior by editing `WORKFLOW.md` — this is the policy layer that controls what the agent does at each workflow phase.
69
91
 
@@ -111,26 +133,54 @@ gh-symphony project add
111
133
 
112
134
  The interactive wizard will:
113
135
 
114
- 1. Authenticate via `gh` CLI
136
+ 1. Authenticate via `GITHUB_GRAPHQL_TOKEN` or fall back to `gh` CLI
115
137
  2. Let you select a **GitHub Project**
116
138
  3. Optionally limit processing to issues assigned to the authenticated user
117
139
  4. Optionally customize advanced settings for repository filtering and workspace root directory
118
140
  5. Write project configuration to `~/.gh-symphony/`
119
141
 
142
+ Token-only non-interactive setup:
143
+
144
+ ```bash
145
+ GITHUB_GRAPHQL_TOKEN=ghp_your_classic_token \
146
+ gh-symphony workflow init --non-interactive --project PVT_xxx --output WORKFLOW.md
147
+
148
+ GITHUB_GRAPHQL_TOKEN=ghp_your_classic_token \
149
+ gh-symphony project add --non-interactive --project PVT_xxx --workspace-dir ~/.gh-symphony/workspaces
150
+ ```
151
+
152
+ Token-only project registration is also supported:
153
+
154
+ ```bash
155
+ export GITHUB_GRAPHQL_TOKEN=ghp_your_classic_token
156
+ gh-symphony project add
157
+ ```
158
+
120
159
  ### Project Management
121
160
 
122
161
  ```bash
123
- gh-symphony doctor # Validate auth, config, WORKFLOW.md, and runtime command
162
+ gh-symphony doctor # Validate local prerequisites, auth, config, WORKFLOW.md, and runtime command
163
+ gh-symphony doctor --fix # Apply safe fixes and guide/launch follow-up recovery commands
124
164
  gh-symphony project list # List all configured projects
125
165
  gh-symphony project remove <id> # Remove a project
166
+ gh-symphony repo sync # Add newly linked repositories from the GitHub Project
167
+ gh-symphony repo sync --dry-run # Preview linked repository drift
168
+ gh-symphony repo sync --prune # Remove local repositories no longer linked
126
169
  ```
127
170
 
171
+ Use `gh-symphony repo sync` when the GitHub Project board has gained or lost
172
+ linked repositories since the project was first added locally. Default sync is
173
+ additive; `--prune` switches to strict alignment, and `--json` prints the added,
174
+ removed, unchanged, and final repository sets.
175
+
128
176
  ## 4. Run the Orchestrator
129
177
 
130
178
  ### Foreground
131
179
 
132
180
  ```bash
133
181
  gh-symphony start
182
+ gh-symphony start --once # Run startup cleanup + one orchestration tick, then exit
183
+ gh-symphony project start --once # Same one-shot flow for an explicit project
134
184
  ```
135
185
 
136
186
  ### Background (daemon)
@@ -140,6 +190,8 @@ gh-symphony start --daemon # Start in background
140
190
  gh-symphony stop # Stop the daemon
141
191
  ```
142
192
 
193
+ Use `start --once` for the first real managed-project run or a CI smoke check. It reuses the configured GitHub Project binding and `WORKFLOW.md` and performs exactly one poll/reconcile/dispatch cycle instead of entering the long-running orchestration loop. `--daemon --once` is rejected because the modes conflict. If you add `--http`, the dashboard/API remains available after that one-shot tick completes, and the process stays up until you interrupt it with `Ctrl+C`.
194
+
143
195
  ### Monitor
144
196
 
145
197
  ```bash
@@ -164,32 +216,50 @@ gh-symphony recover --dry-run # Preview what would be recovered
164
216
 
165
217
  ## Diagnostics
166
218
 
167
- `gh-symphony doctor` validates the most common first-run prerequisites in one pass:
219
+ `gh-symphony doctor` validates the most common first-run prerequisites in one pass. `gh-symphony doctor --fix` extends that flow with safe remediation and guided follow-up:
220
+
221
+ - creates missing config/runtime/workspace directories
222
+ - launches `gh auth login` or `gh auth refresh` when a TTY is available, otherwise prints the exact command to run
223
+ - launches `gh-symphony init` when `WORKFLOW.md` is missing or invalid
224
+ - launches `gh-symphony project add` when managed project setup or GitHub Project binding must be repaired
225
+ - prints concrete runtime install guidance when the configured command is missing on `PATH`
168
226
 
169
- - `gh` installation, auth, and required scopes
227
+ The diagnostic checks cover:
228
+
229
+ - the active GitHub auth source (`GITHUB_GRAPHQL_TOKEN` first, otherwise `gh`) and required scopes
230
+ - Node.js runtime version against the documented minimum (`v24+`) and the current `process.version`
231
+ - Git installation availability on `PATH`, including `git --version` when available
232
+ - GitHub authentication via `GITHUB_GRAPHQL_TOKEN` or `gh`, including required scopes
170
233
  - managed project selection plus GitHub Project binding resolution
171
234
  - config/runtime/workspace path writability
172
235
  - repository `WORKFLOW.md` presence and parse validity
173
236
  - runtime command availability on `PATH`
174
237
 
175
- Use JSON output for scripts and CI smoke checks:
238
+ Use JSON output for scripts and CI smoke checks. `--fix --json` includes a remediation section where each step is reported as `applied`, `skipped`, or `manual`.
176
239
 
177
240
  ```bash
178
241
  gh-symphony doctor --json
242
+ gh-symphony doctor --fix --json
243
+ gh-symphony start --once
179
244
  ```
180
245
 
246
+ JSON output includes the resolved auth source as `env` or `gh`.
247
+
181
248
  ## Command Reference
182
249
 
183
250
  ```
184
251
  Setup:
185
- init Interactive repository setup wizard
186
- doctor Run first-run diagnostics
252
+ workflow init Interactive repository setup wizard
253
+ workflow validate Parse and strictly validate WORKFLOW.md
254
+ workflow preview Render the final worker prompt from a sample issue
255
+ doctor Run diagnostics and optional first-run remediation
187
256
  config show Show current configuration
188
257
  config set Set a configuration value
189
258
  config edit Open config in $EDITOR
190
259
 
191
260
  Orchestration:
192
261
  start Start the orchestrator (foreground)
262
+ start --once Run a single orchestration tick and exit
193
263
  start --daemon Start the orchestrator (background)
194
264
  stop Stop the background orchestrator
195
265
  status Show orchestrator status
@@ -202,6 +272,7 @@ Project Management:
202
272
  project add Add a new project (interactive wizard)
203
273
  project list List all configured projects
204
274
  project remove Remove a project
275
+ repo sync Refresh repositories from the linked GitHub Project
205
276
 
206
277
  Global Options:
207
278
  --config <dir> Config directory (default: ~/.gh-symphony)
@@ -3,38 +3,33 @@ import {
3
3
  abortIfCancelled,
4
4
  generateProjectId,
5
5
  writeConfig
6
- } from "./chunk-5YLETHMR.js";
6
+ } from "./chunk-RN2PACNV.js";
7
7
  import {
8
+ start_default
9
+ } from "./chunk-EKKT5USP.js";
10
+ import {
11
+ GhAuthError,
8
12
  GitHubScopeError,
9
13
  checkRequiredScopes,
10
14
  createClient,
15
+ getGhTokenWithSource,
11
16
  getProjectDetail,
12
17
  listUserProjects,
18
+ resolveGitHubAuth,
13
19
  validateToken
14
- } from "./chunk-62L6QQE6.js";
15
- import {
16
- start_default
17
- } from "./chunk-ZYYY55WB.js";
18
- import {
19
- GhAuthError,
20
- ensureGhAuth,
21
- getGhToken
22
- } from "./chunk-7UBUBSMH.js";
23
- import {
24
- stop_default
25
- } from "./chunk-Y6TYJMNT.js";
20
+ } from "./chunk-TILHWBP6.js";
26
21
  import {
27
22
  status_default
28
23
  } from "./chunk-XN5ABWZ6.js";
29
24
  import {
30
25
  stripAnsi
31
26
  } from "./chunk-MVRF7BES.js";
32
- import "./chunk-LZE6YUSB.js";
33
- import "./chunk-OL73UN2X.js";
34
27
  import {
35
28
  resolveRuntimeRoot
36
29
  } from "./chunk-5NV3LSAJ.js";
37
- import "./chunk-C7G7RJ4G.js";
30
+ import {
31
+ stop_default
32
+ } from "./chunk-Y6TYJMNT.js";
38
33
  import {
39
34
  daemonPidPath,
40
35
  httpStatusPath,
@@ -68,7 +63,7 @@ Then re-run: ${retryCommand}`,
68
63
  );
69
64
  }
70
65
  function parseProjectAddFlags(args) {
71
- const flags = { nonInteractive: false, assignedOnly: false };
66
+ const flags = { nonInteractive: false };
72
67
  for (let i = 0; i < args.length; i += 1) {
73
68
  const arg = args[i];
74
69
  const next = args[i + 1];
@@ -308,12 +303,12 @@ async function projectAdd(args, options) {
308
303
  await projectAddNonInteractive(flags, options);
309
304
  return;
310
305
  }
311
- await projectAddInteractive(options);
306
+ await projectAddInteractive(flags, options);
312
307
  }
313
308
  async function projectAddNonInteractive(flags, options) {
314
309
  let token;
315
310
  try {
316
- token = getGhToken();
311
+ token = getGhTokenWithSource().token;
317
312
  } catch {
318
313
  process.stderr.write(
319
314
  "Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n"
@@ -381,7 +376,7 @@ async function projectAddNonInteractive(flags, options) {
381
376
  `);
382
377
  }
383
378
  }
384
- async function projectAddInteractive(options) {
379
+ async function projectAddInteractive(flags, options) {
385
380
  p.intro("gh-symphony - Project Setup");
386
381
  const defaultWorkspaceDir = join(options.configDir, "workspaces");
387
382
  const existingConfig = await loadGlobalConfig(options.configDir);
@@ -399,32 +394,19 @@ async function projectAddInteractive(options) {
399
394
  }
400
395
  }
401
396
  const s1 = p.spinner();
402
- s1.start("Checking gh CLI authentication...");
397
+ s1.start("Checking GitHub authentication...");
403
398
  let login;
404
399
  let client;
405
400
  try {
406
- const { login: ghLogin, token } = ensureGhAuth();
407
- login = ghLogin;
408
- client = createClient(token);
409
- s1.stop(`Authenticated as ${login}`);
401
+ const auth = await resolveGitHubAuth();
402
+ const sourceLabel = auth.source === "env" ? "GITHUB_GRAPHQL_TOKEN" : "gh CLI";
403
+ login = auth.login;
404
+ client = createClient(auth.token);
405
+ s1.stop(`Authenticated via ${sourceLabel} as ${login}`);
410
406
  } catch (error) {
411
407
  s1.stop("Authentication failed.");
412
408
  if (error instanceof GhAuthError) {
413
- if (error.code === "not_installed") {
414
- p.log.error(
415
- "gh CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. https://cli.github.com \uC5D0\uC11C \uC124\uCE58\uD558\uC138\uC694."
416
- );
417
- } else if (error.code === "not_authenticated") {
418
- p.log.error(
419
- "gh auth login --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
420
- );
421
- } else if (error.code === "missing_scopes") {
422
- p.log.error(
423
- "gh auth refresh --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
424
- );
425
- } else {
426
- p.log.error(error.message);
427
- }
409
+ p.log.error(error.message);
428
410
  } else {
429
411
  p.log.error(error instanceof Error ? error.message : "Unknown error");
430
412
  }
@@ -486,10 +468,64 @@ async function projectAddInteractive(options) {
486
468
  process.exitCode = 1;
487
469
  return;
488
470
  }
471
+ const {
472
+ assignedOnly: promptAssignedOnly,
473
+ selectedRepos,
474
+ workspaceDir
475
+ } = await promptProjectRegistrationOptions({
476
+ projectDetail,
477
+ defaultWorkspaceDir,
478
+ assignedOnlyMessage: "Step 2/2 - Only process issues assigned to the authenticated GitHub user?",
479
+ assignedOnlyInitialValue: flags.assignedOnly
480
+ });
481
+ const assignedOnly = flags.assignedOnly || promptAssignedOnly;
482
+ const repoSummary = selectedRepos.length === projectDetail.linkedRepositories.length ? `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (all ${selectedRepos.length} linked)` : `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (${selectedRepos.length} of ${projectDetail.linkedRepositories.length} linked)`;
483
+ p.note(
484
+ renderProjectRegistrationSummary({
485
+ login,
486
+ projectTitle: projectDetail.title,
487
+ repoSummary,
488
+ assignedOnly,
489
+ workspaceDir
490
+ }),
491
+ "Configuration Summary"
492
+ );
493
+ const confirmed = await abortIfCancelled(
494
+ p.confirm({ message: "Apply this configuration?" })
495
+ );
496
+ if (!confirmed) {
497
+ p.cancel("Setup cancelled.");
498
+ process.exitCode = 130;
499
+ return;
500
+ }
501
+ const projectId = generateProjectId(projectDetail.title, projectDetail.id);
502
+ const s6 = p.spinner();
503
+ s6.start("Writing configuration...");
504
+ try {
505
+ await writeConfig(options.configDir, {
506
+ projectId,
507
+ project: projectDetail,
508
+ repos: selectedRepos,
509
+ workspaceDir,
510
+ assignedOnly
511
+ });
512
+ s6.stop("Configuration saved.");
513
+ } catch (error) {
514
+ s6.stop("Failed to write configuration.");
515
+ p.log.error(error instanceof Error ? error.message : "Unknown error");
516
+ process.exitCode = 1;
517
+ return;
518
+ }
519
+ p.outro(
520
+ `Project "${projectId}" created.
521
+ Run 'gh-symphony start' to begin orchestration.`
522
+ );
523
+ }
524
+ async function promptProjectRegistrationOptions(input) {
489
525
  const assignedOnly = await abortIfCancelled(
490
526
  p.confirm({
491
- message: "Step 2/2 - Only process issues assigned to the authenticated GitHub user?",
492
- initialValue: false
527
+ message: input.assignedOnlyMessage ?? "Only process issues assigned to the authenticated GitHub user?",
528
+ initialValue: input.assignedOnlyInitialValue ?? false
493
529
  })
494
530
  );
495
531
  const customizeAdvancedOptions = await abortIfCancelled(
@@ -498,8 +534,8 @@ async function projectAddInteractive(options) {
498
534
  initialValue: false
499
535
  })
500
536
  );
501
- let selectedRepos = projectDetail.linkedRepositories;
502
- let workspaceDir = defaultWorkspaceDir;
537
+ let selectedRepos = input.projectDetail.linkedRepositories;
538
+ let workspaceDir = input.defaultWorkspaceDir;
503
539
  if (customizeAdvancedOptions) {
504
540
  const filterRepositories = await abortIfCancelled(
505
541
  p.confirm({
@@ -511,7 +547,7 @@ async function projectAddInteractive(options) {
511
547
  selectedRepos = await abortIfCancelled(
512
548
  p.multiselect({
513
549
  message: "Select repositories to orchestrate:",
514
- options: projectDetail.linkedRepositories.map((repo) => ({
550
+ options: input.projectDetail.linkedRepositories.map((repo) => ({
515
551
  value: repo,
516
552
  label: `${repo.owner}/${repo.name}`
517
553
  })),
@@ -522,55 +558,28 @@ async function projectAddInteractive(options) {
522
558
  workspaceDir = await abortIfCancelled(
523
559
  p.text({
524
560
  message: "Workspace root directory:",
525
- placeholder: defaultWorkspaceDir,
526
- defaultValue: defaultWorkspaceDir,
561
+ placeholder: input.defaultWorkspaceDir,
562
+ defaultValue: input.defaultWorkspaceDir,
527
563
  validate(value) {
528
564
  return value.trim().length > 0 ? void 0 : "Workspace directory is required.";
529
565
  }
530
566
  })
531
567
  );
532
568
  }
533
- const repoSummary = selectedRepos.length === projectDetail.linkedRepositories.length ? `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (all ${selectedRepos.length} linked)` : `${selectedRepos.map((repo) => `${repo.owner}/${repo.name}`).join(", ")} (${selectedRepos.length} of ${projectDetail.linkedRepositories.length} linked)`;
534
- p.note(
535
- [
536
- `User: ${login}`,
537
- `Project: ${projectDetail.title}`,
538
- `Repos: ${repoSummary}`,
539
- `Assigned: ${assignedOnly ? `Only issues assigned to ${login}` : "All project issues"}`,
540
- `Workspace: ${workspaceDir}`
541
- ].join("\n"),
542
- "Configuration Summary"
543
- );
544
- const confirmed = await abortIfCancelled(
545
- p.confirm({ message: "Apply this configuration?" })
546
- );
547
- if (!confirmed) {
548
- p.cancel("Setup cancelled.");
549
- process.exitCode = 130;
550
- return;
551
- }
552
- const projectId = generateProjectId(projectDetail.title, projectDetail.id);
553
- const s6 = p.spinner();
554
- s6.start("Writing configuration...");
555
- try {
556
- await writeConfig(options.configDir, {
557
- projectId,
558
- project: projectDetail,
559
- repos: selectedRepos,
560
- workspaceDir,
561
- assignedOnly
562
- });
563
- s6.stop("Configuration saved.");
564
- } catch (error) {
565
- s6.stop("Failed to write configuration.");
566
- p.log.error(error instanceof Error ? error.message : "Unknown error");
567
- process.exitCode = 1;
568
- return;
569
- }
570
- p.outro(
571
- `Project "${projectId}" created.
572
- Run 'gh-symphony start' to begin orchestration.`
573
- );
569
+ return {
570
+ assignedOnly,
571
+ selectedRepos,
572
+ workspaceDir
573
+ };
574
+ }
575
+ function renderProjectRegistrationSummary(input) {
576
+ return [
577
+ `User: ${input.login}`,
578
+ `Project: ${input.projectTitle}`,
579
+ `Repos: ${input.repoSummary}`,
580
+ `Assigned: ${input.assignedOnly ? `Only issues assigned to ${input.login}` : "All project issues"}`,
581
+ `Workspace: ${input.workspaceDir}`
582
+ ].join("\n");
574
583
  }
575
584
  async function projectList(options) {
576
585
  const global = await loadGlobalConfig(options.configDir);
@@ -650,7 +659,9 @@ async function projectRemove(args, options) {
650
659
  async function projectSwitch(options) {
651
660
  const global = await loadGlobalConfig(options.configDir);
652
661
  if (!global || global.projects.length === 0) {
653
- process.stderr.write("No projects configured. Run 'gh-symphony init'.\n");
662
+ process.stderr.write(
663
+ "No projects configured. Run 'gh-symphony workflow init'.\n"
664
+ );
654
665
  process.exitCode = 1;
655
666
  return;
656
667
  }
@@ -676,6 +687,9 @@ async function projectSwitch(options) {
676
687
  process.stdout.write(`Switched to project: ${selected}
677
688
  `);
678
689
  }
690
+
679
691
  export {
680
- project_default as default
692
+ project_default,
693
+ promptProjectRegistrationOptions,
694
+ renderProjectRegistrationSummary
681
695
  };
@@ -1,23 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- getGhToken
4
- } from "./chunk-7UBUBSMH.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-LZE6YUSB.js";
8
+ } from "./chunk-HZVDTAPS.js";
21
9
  import {
22
10
  deriveIssueWorkspaceKeyFromIdentifier,
23
11
  isFileMissing,
@@ -26,7 +14,19 @@ import {
26
14
  parseRecentEvents,
27
15
  readJsonFile,
28
16
  safeReadDir
29
- } from "./chunk-OL73UN2X.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";
@@ -387,7 +387,8 @@ var DEFAULT_HTTP_PORT = 4680;
387
387
  var HTTP_HOST = "0.0.0.0";
388
388
  function parseStartArgs(args) {
389
389
  const parsed = {
390
- daemon: false
390
+ daemon: false,
391
+ once: false
391
392
  };
392
393
  for (let i = 0; i < args.length; i += 1) {
393
394
  const arg = args[i];
@@ -395,6 +396,10 @@ function parseStartArgs(args) {
395
396
  parsed.daemon = true;
396
397
  continue;
397
398
  }
399
+ if (arg === "--once") {
400
+ parsed.once = true;
401
+ continue;
402
+ }
398
403
  if (arg === "--http") {
399
404
  const value = args[i + 1];
400
405
  if (!value || value.startsWith("-")) {
@@ -649,11 +654,16 @@ var handler = async (args, options) => {
649
654
  process.stderr.write(`${parsed.error}
650
655
  `);
651
656
  process.stderr.write(
652
- "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"
653
658
  );
654
659
  process.exitCode = 2;
655
660
  return;
656
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
+ }
657
667
  const projectConfig = await resolveManagedProjectConfig({
658
668
  configDir: options.configDir,
659
669
  requestedProjectId: parsed.projectId
@@ -761,14 +771,20 @@ var handler = async (args, options) => {
761
771
  `HTTP dashboard listening on ${httpServer.url}`
762
772
  );
763
773
  }
764
- 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
+ );
765
778
  let shuttingDown = false;
766
779
  let shutdownPromise = null;
780
+ let keepHttpAliveResolve = null;
767
781
  const shutdown = async () => {
768
782
  if (shuttingDown) {
769
783
  return shutdownPromise;
770
784
  }
771
785
  shuttingDown = true;
786
+ keepHttpAliveResolve?.();
787
+ keepHttpAliveResolve = null;
772
788
  const heldLock = projectLock;
773
789
  projectLock = null;
774
790
  shutdownPromise = shutdownForegroundOrchestrator({
@@ -780,16 +796,31 @@ var handler = async (args, options) => {
780
796
  });
781
797
  return shutdownPromise;
782
798
  };
783
- process.on("SIGINT", () => {
799
+ const handleSigint = () => {
784
800
  void shutdown();
785
- });
786
- process.on("SIGTERM", () => {
801
+ };
802
+ const handleSigterm = () => {
787
803
  void shutdown();
788
- });
804
+ };
805
+ process.on("SIGINT", handleSigint);
806
+ process.on("SIGTERM", handleSigterm);
789
807
  try {
790
808
  while (!shuttingDown) {
791
809
  try {
792
- 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
+ }
793
824
  break;
794
825
  } catch (error) {
795
826
  if (shuttingDown) {
@@ -798,12 +829,32 @@ var handler = async (args, options) => {
798
829
  logLine(
799
830
  red("\u2717"),
800
831
  red(
801
- `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"}`
802
833
  )
803
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
+ }
804
853
  }
805
854
  }
806
855
  } finally {
856
+ process.off("SIGINT", handleSigint);
857
+ process.off("SIGTERM", handleSigterm);
807
858
  if (shutdownPromise) {
808
859
  await shutdownPromise;
809
860
  }