@gh-symphony/cli 0.0.5 → 0.0.7

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
@@ -48,12 +48,12 @@ You can further customize the agent's behavior by editing `WORKFLOW.md` — this
48
48
 
49
49
  > Currently supported runtimes: **Codex**, **Claude Code**
50
50
 
51
- ## 3. Set Orchestrator Runner (Tenant)
51
+ ## 3. Set Orchestrator Runner (Project)
52
52
 
53
- On the machine where you want the orchestrator to run, register a tenant:
53
+ On the machine where you want the orchestrator to run, register a project:
54
54
 
55
55
  ```bash
56
- gh-symphony tenant add
56
+ gh-symphony project add
57
57
  ```
58
58
 
59
59
  The interactive wizard will:
@@ -61,15 +61,15 @@ The interactive wizard will:
61
61
  1. Authenticate via `gh` CLI
62
62
  2. Let you select a **GitHub Project**
63
63
  3. Select repositories to orchestrate
64
- 4. Auto-detect workflow column mappings
65
- 5. Choose an AI runtime (Codex / Claude Code / custom)
66
- 6. Write tenant configuration to `~/.gh-symphony/`
64
+ 4. Optionally limit processing to issues assigned to the authenticated user
65
+ 5. Configure the workspace root directory
66
+ 6. Write project configuration to `~/.gh-symphony/`
67
67
 
68
- ### Tenant Management
68
+ ### Project Management
69
69
 
70
70
  ```bash
71
- gh-symphony tenant list # List all configured tenants
72
- gh-symphony tenant remove <id> # Remove a tenant
71
+ gh-symphony project list # List all configured projects
72
+ gh-symphony project remove <id> # Remove a project
73
73
  ```
74
74
 
75
75
  ## 4. Run the Orchestrator
@@ -127,10 +127,10 @@ Orchestration:
127
127
  recover Recover stalled runs
128
128
  logs View orchestrator logs
129
129
 
130
- Tenant Management:
131
- tenant add Add a new tenant (interactive wizard)
132
- tenant list List all configured tenants
133
- tenant remove Remove a tenant
130
+ Project Management:
131
+ project add Add a new project (interactive wizard)
132
+ project list List all configured projects
133
+ project remove Remove a project
134
134
 
135
135
  Global Options:
136
136
  --config <dir> Config directory (default: ~/.gh-symphony)
@@ -31,12 +31,12 @@ async function configShow(options) {
31
31
  return;
32
32
  }
33
33
  process.stdout.write(`Config: ${configFilePath(options.configDir)}\n\n`);
34
- process.stdout.write(`Active tenant: ${config.activeTenant ?? "none"}\n`);
35
- process.stdout.write(`Tenants: ${config.tenants.join(", ") || "none"}\n`);
34
+ process.stdout.write(`Active project: ${config.activeProject ?? "none"}\n`);
35
+ process.stdout.write(`Projects: ${config.projects.join(", ") || "none"}\n`);
36
36
  }
37
37
  // ── 7.2: config set ──────────────────────────────────────────────────────────
38
38
  const VALID_KEYS = {
39
- "active-tenant": { type: "string" },
39
+ "active-project": { type: "string" },
40
40
  };
41
41
  async function configSet(args, options) {
42
42
  const [key, value] = args;
@@ -54,17 +54,17 @@ async function configSet(args, options) {
54
54
  }
55
55
  const config = (await loadGlobalConfig(options.configDir)) ??
56
56
  {
57
- activeTenant: null,
58
- tenants: [],
57
+ activeProject: null,
58
+ projects: [],
59
59
  };
60
60
  switch (key) {
61
- case "active-tenant":
62
- if (!config.tenants.includes(value)) {
63
- process.stderr.write(`Tenant "${value}" not found. Available: ${config.tenants.join(", ")}\n`);
61
+ case "active-project":
62
+ if (!config.projects.includes(value)) {
63
+ process.stderr.write(`Project "${value}" not found. Available: ${config.projects.join(", ")}\n`);
64
64
  process.exitCode = 1;
65
65
  return;
66
66
  }
67
- config.activeTenant = value;
67
+ config.activeProject = value;
68
68
  break;
69
69
  }
70
70
  await saveGlobalConfig(options.configDir, config);
@@ -4,7 +4,7 @@ gh-symphony — AI Coding Agent Orchestrator
4
4
  Usage: gh-symphony <command> [options]
5
5
 
6
6
  Setup:
7
- init Interactive tenant setup wizard
7
+ init Interactive project setup wizard
8
8
  config show Show current configuration
9
9
  config set Set a configuration value
10
10
  config edit Open config in $EDITOR
@@ -18,15 +18,15 @@ Orchestration:
18
18
  recover Recover stalled runs
19
19
  logs View orchestrator logs
20
20
 
21
- Tenant Management:
22
- tenant add Add a new tenant (interactive wizard)
23
- tenant list List all configured tenants
24
- tenant remove Remove a tenant
21
+ Project Management:
22
+ project add Add a new project (interactive wizard)
23
+ project list List all configured projects
24
+ project remove Remove a project
25
25
 
26
26
  Project / Repo:
27
- project list List tenants
28
- project switch Switch active tenant
29
- project status Show active tenant details
27
+ project list List projects
28
+ project switch Switch active project
29
+ project status Show orchestrator status for a project
30
30
  repo list List configured repositories
31
31
  repo add Add a repository
32
32
  repo remove Remove a repository
@@ -40,10 +40,10 @@ Global Options:
40
40
  --version, -V Show version
41
41
 
42
42
  Examples:
43
- gh-symphony tenant add # Add a tenant (interactive)
44
- gh-symphony tenant add --non-interactive --project <id>
45
- gh-symphony tenant list # List all tenants
46
- gh-symphony tenant remove <id> # Remove a tenant
43
+ gh-symphony project add # Add a project (interactive)
44
+ gh-symphony project add --non-interactive --project <id> --workspace-dir <path>
45
+ gh-symphony project list # List all projects
46
+ gh-symphony project remove <id> # Remove a project
47
47
  gh-symphony start # Start orchestrator
48
48
  gh-symphony start --daemon # Start in background
49
49
  gh-symphony run org/repo#123 # Dispatch a specific issue
@@ -1,6 +1,5 @@
1
1
  import type { GlobalOptions } from "../index.js";
2
2
  import { type ProjectDetail, type ProjectStatusField, type LinkedRepository } from "../github/client.js";
3
- import { type StateMapping } from "../config.js";
4
3
  export declare function abortIfCancelled<T>(input: T | Promise<T>): Promise<Exclude<T, symbol>>;
5
4
  declare const handler: (args: string[], options: GlobalOptions) => Promise<void>;
6
5
  export default handler;
@@ -14,7 +13,7 @@ type EcosystemOptions = {
14
13
  };
15
14
  export type EcosystemResult = {
16
15
  projectId: string;
17
- projectTitle: string;
16
+ githubProjectTitle: string;
18
17
  runtime: string;
19
18
  skillsDir: string | null;
20
19
  contextYamlWritten: boolean;
@@ -24,26 +23,12 @@ export type EcosystemResult = {
24
23
  };
25
24
  export declare function writeEcosystem(opts: EcosystemOptions): Promise<EcosystemResult>;
26
25
  type WriteConfigInput = {
27
- tenantId: string;
26
+ projectId: string;
28
27
  project: ProjectDetail;
29
28
  repos: LinkedRepository[];
30
- statusField: {
31
- id: string;
32
- name: string;
33
- options: Array<{
34
- id: string;
35
- name: string;
36
- color?: string | null;
37
- }>;
38
- };
39
- mappings: Record<string, StateMapping>;
40
- runtime: string;
41
- agentCommand?: string;
42
- workerCommand?: string;
43
- pollIntervalMs?: number;
44
- concurrency?: number;
29
+ workspaceDir: string;
45
30
  maxAttempts?: number;
46
31
  assignedOnly?: boolean;
47
32
  };
48
33
  export declare function writeConfig(configDir: string, input: WriteConfigInput): Promise<void>;
49
- export declare function generateTenantId(projectTitle: string, uniqueKey: string): string;
34
+ export declare function generateProjectId(githubProjectTitle: string, uniqueKey: string): string;
@@ -2,11 +2,10 @@ import * as p from "@clack/prompts";
2
2
  import { createHash } from "node:crypto";
3
3
  import { mkdir, rename, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, join, relative, resolve } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
5
  import { createClient, validateToken, checkRequiredScopes, listUserProjects, getProjectDetail, GitHubScopeError, } from "../github/client.js";
7
6
  import { inferAllStateRoles, toWorkflowLifecycleConfig, validateStateMapping, } from "../mapping/smart-defaults.js";
8
7
  import { generateWorkflowMarkdown } from "../workflow/generate-workflow-md.js";
9
- import { loadGlobalConfig, saveGlobalConfig, saveTenantConfig, saveWorkflowMapping, } from "../config.js";
8
+ import { loadGlobalConfig, saveGlobalConfig, saveProjectConfig, } from "../config.js";
10
9
  import { getGhToken, ensureGhAuth, GhAuthError } from "../github/gh-auth.js";
11
10
  import { detectEnvironment } from "../detection/environment-detector.js";
12
11
  import { buildContextYaml, writeContextYaml, } from "../context/generate-context-yaml.js";
@@ -121,7 +120,7 @@ export async function writeEcosystem(opts) {
121
120
  const result = await writeAllSkills(cwd, runtime, ALL_SKILL_TEMPLATES, {
122
121
  runtime,
123
122
  projectId: projectDetail.id,
124
- projectTitle: projectDetail.title,
123
+ githubProjectTitle: projectDetail.title,
125
124
  repositories: projectDetail.linkedRepositories.map((r) => ({
126
125
  owner: r.owner,
127
126
  name: r.name,
@@ -140,7 +139,7 @@ export async function writeEcosystem(opts) {
140
139
  }
141
140
  return {
142
141
  projectId: projectDetail.id,
143
- projectTitle: projectDetail.title,
142
+ githubProjectTitle: projectDetail.title,
144
143
  runtime,
145
144
  skillsDir,
146
145
  contextYamlWritten,
@@ -154,7 +153,7 @@ function printEcosystemSummary(result, workflowPath, opts) {
154
153
  const cwd = process.cwd();
155
154
  const relWorkflow = relative(cwd, workflowPath) || "WORKFLOW.md";
156
155
  const lines = [];
157
- lines.push(`Project ${result.projectTitle} (${result.projectId})`);
156
+ lines.push(`GitHub Project ${result.githubProjectTitle} (${result.projectId})`);
158
157
  lines.push(`Runtime ${result.runtime}`);
159
158
  lines.push("");
160
159
  lines.push("Generated files");
@@ -218,7 +217,7 @@ async function runNonInteractive(flags, options) {
218
217
  }
219
218
  // Find project
220
219
  const projects = await listUserProjects(client);
221
- let project;
220
+ let githubProject;
222
221
  if (flags.project) {
223
222
  const match = projects.find((proj) => proj.id === flags.project || proj.url === flags.project);
224
223
  if (!match) {
@@ -226,10 +225,10 @@ async function runNonInteractive(flags, options) {
226
225
  process.exitCode = 1;
227
226
  return;
228
227
  }
229
- project = await getProjectDetail(client, match.id);
228
+ githubProject = await getProjectDetail(client, match.id);
230
229
  }
231
230
  else if (projects.length === 1) {
232
- project = await getProjectDetail(client, projects[0].id);
231
+ githubProject = await getProjectDetail(client, projects[0].id);
233
232
  }
234
233
  else {
235
234
  process.stderr.write("Error: --project is required when multiple projects exist.\n");
@@ -237,8 +236,8 @@ async function runNonInteractive(flags, options) {
237
236
  return;
238
237
  }
239
238
  // Auto-map with smart defaults
240
- const statusField = project.statusFields.find((f) => f.name.toLowerCase() === "status") ??
241
- project.statusFields[0];
239
+ const statusField = githubProject.statusFields.find((f) => f.name.toLowerCase() === "status") ??
240
+ githubProject.statusFields[0];
242
241
  if (!statusField) {
243
242
  process.stderr.write("Error: No status field found on the project.\n");
244
243
  process.exitCode = 1;
@@ -261,7 +260,7 @@ async function runNonInteractive(flags, options) {
261
260
  const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
262
261
  const outputPath = resolve(flags.output ?? "WORKFLOW.md");
263
262
  const workflowMd = generateWorkflowMarkdown({
264
- projectId: project.id,
263
+ projectId: githubProject.id,
265
264
  stateFieldName: statusField.name,
266
265
  mappings,
267
266
  lifecycle: lifecycleConfig,
@@ -270,7 +269,7 @@ async function runNonInteractive(flags, options) {
270
269
  await writeFile(outputPath, workflowMd, "utf8");
271
270
  const ecosystemResult = await writeEcosystem({
272
271
  cwd: process.cwd(),
273
- projectDetail: project,
272
+ projectDetail: githubProject,
274
273
  statusField,
275
274
  runtime: "codex",
276
275
  skipSkills: flags.skipSkills,
@@ -282,7 +281,7 @@ async function runNonInteractive(flags, options) {
282
281
  else {
283
282
  printEcosystemSummary(ecosystemResult, outputPath, {
284
283
  interactive: false,
285
- nextSteps: "Run 'gh-symphony tenant add' to register a tenant.",
284
+ nextSteps: "Run 'gh-symphony project add' to register a project.",
286
285
  });
287
286
  }
288
287
  }
@@ -347,8 +346,8 @@ async function runInteractiveStandalone(_options) {
347
346
  process.exitCode = 1;
348
347
  return;
349
348
  }
350
- const selectedProjectId = await abortIfCancelled(p.select({
351
- message: "Step 1/2 — Select a GitHub Project:",
349
+ const selectedGithubProjectId = await abortIfCancelled(p.select({
350
+ message: "Step 1/2 — Select a GitHub Project board:",
352
351
  options: projects.map((proj) => ({
353
352
  value: proj.id,
354
353
  label: `${proj.owner.login}/${proj.title}`,
@@ -360,7 +359,7 @@ async function runInteractiveStandalone(_options) {
360
359
  s2d.start("Loading project details...");
361
360
  let projectDetail;
362
361
  try {
363
- projectDetail = await getProjectDetail(client, selectedProjectId);
362
+ projectDetail = await getProjectDetail(client, selectedGithubProjectId);
364
363
  s2d.stop(`Loaded: ${projectDetail.title}`);
365
364
  }
366
365
  catch (error) {
@@ -433,32 +432,15 @@ async function runInteractiveStandalone(_options) {
433
432
  });
434
433
  printEcosystemSummary(ecosystemResult, outputPath, {
435
434
  interactive: true,
436
- nextSteps: "Run 'gh-symphony tenant add' to register a tenant.",
435
+ nextSteps: "Run 'gh-symphony project add' to register a project.",
437
436
  });
438
437
  }
439
- function resolveWorkerCommand() {
440
- try {
441
- const url = import.meta.resolve("@gh-symphony/worker/dist/index.js");
442
- return `node ${fileURLToPath(url)}`;
443
- }
444
- catch {
445
- return undefined;
446
- }
447
- }
448
438
  export async function writeConfig(configDir, input) {
449
- const lifecycleConfig = toWorkflowLifecycleConfig(input.statusField.name, input.mappings);
450
- // Save workflow mapping
451
- const mappingConfig = {
452
- stateFieldName: input.statusField.name,
453
- mappings: input.mappings,
454
- lifecycle: lifecycleConfig,
455
- };
456
- await saveWorkflowMapping(configDir, input.tenantId, mappingConfig);
457
- // Save tenant config (OrchestratorTenantConfig shape)
458
- const runtimeDir = `${configDir}/tenants/${input.tenantId}/runtime`;
459
- await saveTenantConfig(configDir, input.tenantId, {
460
- tenantId: input.tenantId,
461
- slug: input.tenantId,
439
+ await saveProjectConfig(configDir, input.projectId, {
440
+ projectId: input.projectId,
441
+ slug: input.projectId,
442
+ displayName: input.project.title,
443
+ workspaceDir: input.workspaceDir,
462
444
  repositories: input.repos.map((r) => ({
463
445
  owner: r.owner,
464
446
  name: r.name,
@@ -472,43 +454,24 @@ export async function writeConfig(configDir, input) {
472
454
  ...(input.assignedOnly ? { assignedOnly: true } : {}),
473
455
  },
474
456
  },
475
- runtime: {
476
- driver: "local",
477
- workspaceRuntimeDir: runtimeDir,
478
- projectRoot: process.cwd(),
479
- workerCommand: input.workerCommand ?? resolveWorkerCommand(),
480
- },
481
- workflowMapping: mappingConfig,
482
457
  });
483
458
  // Save/update global config
484
459
  const existing = await loadGlobalConfig(configDir);
485
460
  const globalConfig = {
486
- activeTenant: input.tenantId,
487
- tenants: [
488
- ...(existing?.tenants ?? []).filter((t) => t !== input.tenantId),
489
- input.tenantId,
461
+ activeProject: input.projectId,
462
+ projects: [
463
+ ...(existing?.projects ?? []).filter((t) => t !== input.projectId),
464
+ input.projectId,
490
465
  ],
491
466
  };
492
467
  await saveGlobalConfig(configDir, globalConfig);
493
- // Generate WORKFLOW.md for tenant-level fallback
494
- const workflowMd = generateWorkflowMarkdown({
495
- projectId: input.project.id,
496
- stateFieldName: input.statusField.name,
497
- mappings: input.mappings,
498
- lifecycle: lifecycleConfig,
499
- runtime: input.agentCommand ?? input.runtime,
500
- pollIntervalMs: input.pollIntervalMs,
501
- concurrency: input.concurrency,
502
- });
503
- const workflowMdPath = join(configDir, "tenants", input.tenantId, "WORKFLOW.md");
504
- await writeFile(workflowMdPath, workflowMd, "utf8");
505
468
  }
506
- export function generateTenantId(projectTitle, uniqueKey) {
507
- const slug = projectTitle
469
+ export function generateProjectId(githubProjectTitle, uniqueKey) {
470
+ const slug = githubProjectTitle
508
471
  .toLowerCase()
509
472
  .replace(/[^a-z0-9]+/g, "-")
510
473
  .replace(/^-|-$/g, "")
511
474
  .slice(0, 32);
512
475
  const suffix = createHash("sha1").update(uniqueKey).digest("hex").slice(0, 8);
513
- return [slug || "tenant", suffix].join("-");
476
+ return [slug || "project", suffix].join("-");
514
477
  }
@@ -2,7 +2,7 @@ import { readFile, readdir } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { createReadStream } from "node:fs";
4
4
  import { createInterface } from "node:readline";
5
- import { loadActiveTenantConfig, orchestratorLogPath } from "../config.js";
5
+ import { loadActiveProjectConfig, loadProjectConfig, orchestratorLogPath, } from "../config.js";
6
6
  function parseLogsArgs(args) {
7
7
  const parsed = { follow: false };
8
8
  for (let i = 0; i < args.length; i += 1) {
@@ -22,14 +22,20 @@ function parseLogsArgs(args) {
22
22
  parsed.level = args[i + 1];
23
23
  i += 1;
24
24
  }
25
+ if (arg === "--project" || arg === "--project-id") {
26
+ parsed.projectId = args[i + 1];
27
+ i += 1;
28
+ }
25
29
  }
26
30
  return parsed;
27
31
  }
28
32
  const handler = async (args, options) => {
29
33
  const parsed = parseLogsArgs(args);
30
- const tenantConfig = await loadActiveTenantConfig(options.configDir);
31
- if (!tenantConfig) {
32
- process.stderr.write("No tenant configured. Run 'gh-symphony init' first.\n");
34
+ const projectConfig = parsed.projectId
35
+ ? await loadProjectConfig(options.configDir, parsed.projectId)
36
+ : await loadActiveProjectConfig(options.configDir);
37
+ if (!projectConfig) {
38
+ process.stderr.write("No project configured. Run 'gh-symphony project add' first.\n");
33
39
  process.exitCode = 1;
34
40
  return;
35
41
  }
@@ -56,7 +62,7 @@ const handler = async (args, options) => {
56
62
  }
57
63
  // Default: read orchestrator log or scan all events
58
64
  if (parsed.follow) {
59
- const logPath = orchestratorLogPath(options.configDir);
65
+ const logPath = orchestratorLogPath(options.configDir, projectConfig.projectId);
60
66
  try {
61
67
  const stream = createReadStream(logPath, { encoding: "utf8" });
62
68
  const rl = createInterface({ input: stream });
@@ -0,0 +1,6 @@
1
+ import { parseArgs, type ParseArgsOptionsConfig } from "node:util";
2
+ type ParseCliArgsResult = ReturnType<typeof parseArgs> | {
3
+ error: string;
4
+ };
5
+ export declare function parseCliArgs(args: string[], options: ParseArgsOptionsConfig): ParseCliArgsResult;
6
+ export {};
@@ -0,0 +1,20 @@
1
+ import { parseArgs } from "node:util";
2
+ export function parseCliArgs(args, options) {
3
+ try {
4
+ return parseArgs({
5
+ args,
6
+ options,
7
+ allowPositionals: false,
8
+ strict: true,
9
+ });
10
+ }
11
+ catch (error) {
12
+ return { error: formatParseArgsError(error) };
13
+ }
14
+ }
15
+ function formatParseArgsError(error) {
16
+ if (error instanceof Error) {
17
+ return error.message;
18
+ }
19
+ return "Invalid arguments";
20
+ }