@gh-symphony/cli 0.0.6 → 0.0.8

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
@@ -25,6 +25,20 @@ Verify the installation:
25
25
  gh-symphony --version
26
26
  ```
27
27
 
28
+ Enable shell completion:
29
+
30
+ ```bash
31
+ gh-symphony completion bash >> ~/.bashrc
32
+ gh-symphony completion zsh >> ~/.zshrc
33
+ gh-symphony completion fish > ~/.config/fish/completions/gh-symphony.fish
34
+ ```
35
+
36
+ If your `zsh` config does not already initialize completion, add this before the generated script line:
37
+
38
+ ```bash
39
+ autoload -Uz compinit && compinit
40
+ ```
41
+
28
42
  ## 2. Set Repository
29
43
 
30
44
  Navigate to the repository you want to orchestrate, then run:
@@ -48,28 +62,27 @@ You can further customize the agent's behavior by editing `WORKFLOW.md` — this
48
62
 
49
63
  > Currently supported runtimes: **Codex**, **Claude Code**
50
64
 
51
- ## 3. Set Orchestrator Runner (Tenant)
65
+ ## 3. Set Orchestrator Runner (Project)
52
66
 
53
- On the machine where you want the orchestrator to run, register a tenant:
67
+ On the machine where you want the orchestrator to run, register a project:
54
68
 
55
69
  ```bash
56
- gh-symphony tenant add
70
+ gh-symphony project add
57
71
  ```
58
72
 
59
73
  The interactive wizard will:
60
74
 
61
75
  1. Authenticate via `gh` CLI
62
76
  2. Let you select a **GitHub Project**
63
- 3. Select repositories to orchestrate
64
- 4. Optionally limit processing to issues assigned to the authenticated user
65
- 5. Configure the workspace root directory
66
- 6. Write tenant configuration to `~/.gh-symphony/`
77
+ 3. Optionally limit processing to issues assigned to the authenticated user
78
+ 4. Optionally customize advanced settings for repository filtering and workspace root directory
79
+ 5. Write project configuration to `~/.gh-symphony/`
67
80
 
68
- ### Tenant Management
81
+ ### Project Management
69
82
 
70
83
  ```bash
71
- gh-symphony tenant list # List all configured tenants
72
- gh-symphony tenant remove <id> # Remove a tenant
84
+ gh-symphony project list # List all configured projects
85
+ gh-symphony project remove <id> # Remove a project
73
86
  ```
74
87
 
75
88
  ## 4. Run the Orchestrator
@@ -126,11 +139,12 @@ Orchestration:
126
139
  run <issue> Dispatch a single issue
127
140
  recover Recover stalled runs
128
141
  logs View orchestrator logs
142
+ completion <shell> Print shell completion for bash/zsh/fish
129
143
 
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
144
+ Project Management:
145
+ project add Add a new project (interactive wizard)
146
+ project list List all configured projects
147
+ project remove Remove a project
134
148
 
135
149
  Global Options:
136
150
  --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> --workspace-dir <path>
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
@@ -13,7 +13,7 @@ type EcosystemOptions = {
13
13
  };
14
14
  export type EcosystemResult = {
15
15
  projectId: string;
16
- projectTitle: string;
16
+ githubProjectTitle: string;
17
17
  runtime: string;
18
18
  skillsDir: string | null;
19
19
  contextYamlWritten: boolean;
@@ -23,7 +23,7 @@ export type EcosystemResult = {
23
23
  };
24
24
  export declare function writeEcosystem(opts: EcosystemOptions): Promise<EcosystemResult>;
25
25
  type WriteConfigInput = {
26
- tenantId: string;
26
+ projectId: string;
27
27
  project: ProjectDetail;
28
28
  repos: LinkedRepository[];
29
29
  workspaceDir: string;
@@ -31,4 +31,4 @@ type WriteConfigInput = {
31
31
  assignedOnly?: boolean;
32
32
  };
33
33
  export declare function writeConfig(configDir: string, input: WriteConfigInput): Promise<void>;
34
- export declare function generateTenantId(projectTitle: string, uniqueKey: string): string;
34
+ export declare function generateProjectId(githubProjectTitle: string, uniqueKey: string): string;
@@ -5,7 +5,7 @@ import { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { createClient, validateToken, checkRequiredScopes, listUserProjects, getProjectDetail, GitHubScopeError, } from "../github/client.js";
6
6
  import { inferAllStateRoles, toWorkflowLifecycleConfig, validateStateMapping, } from "../mapping/smart-defaults.js";
7
7
  import { generateWorkflowMarkdown } from "../workflow/generate-workflow-md.js";
8
- import { loadGlobalConfig, saveGlobalConfig, saveTenantConfig, } from "../config.js";
8
+ import { loadGlobalConfig, saveGlobalConfig, saveProjectConfig, } from "../config.js";
9
9
  import { getGhToken, ensureGhAuth, GhAuthError } from "../github/gh-auth.js";
10
10
  import { detectEnvironment } from "../detection/environment-detector.js";
11
11
  import { buildContextYaml, writeContextYaml, } from "../context/generate-context-yaml.js";
@@ -120,7 +120,7 @@ export async function writeEcosystem(opts) {
120
120
  const result = await writeAllSkills(cwd, runtime, ALL_SKILL_TEMPLATES, {
121
121
  runtime,
122
122
  projectId: projectDetail.id,
123
- projectTitle: projectDetail.title,
123
+ githubProjectTitle: projectDetail.title,
124
124
  repositories: projectDetail.linkedRepositories.map((r) => ({
125
125
  owner: r.owner,
126
126
  name: r.name,
@@ -139,7 +139,7 @@ export async function writeEcosystem(opts) {
139
139
  }
140
140
  return {
141
141
  projectId: projectDetail.id,
142
- projectTitle: projectDetail.title,
142
+ githubProjectTitle: projectDetail.title,
143
143
  runtime,
144
144
  skillsDir,
145
145
  contextYamlWritten,
@@ -153,7 +153,7 @@ function printEcosystemSummary(result, workflowPath, opts) {
153
153
  const cwd = process.cwd();
154
154
  const relWorkflow = relative(cwd, workflowPath) || "WORKFLOW.md";
155
155
  const lines = [];
156
- lines.push(`Project ${result.projectTitle} (${result.projectId})`);
156
+ lines.push(`GitHub Project ${result.githubProjectTitle} (${result.projectId})`);
157
157
  lines.push(`Runtime ${result.runtime}`);
158
158
  lines.push("");
159
159
  lines.push("Generated files");
@@ -217,7 +217,7 @@ async function runNonInteractive(flags, options) {
217
217
  }
218
218
  // Find project
219
219
  const projects = await listUserProjects(client);
220
- let project;
220
+ let githubProject;
221
221
  if (flags.project) {
222
222
  const match = projects.find((proj) => proj.id === flags.project || proj.url === flags.project);
223
223
  if (!match) {
@@ -225,10 +225,10 @@ async function runNonInteractive(flags, options) {
225
225
  process.exitCode = 1;
226
226
  return;
227
227
  }
228
- project = await getProjectDetail(client, match.id);
228
+ githubProject = await getProjectDetail(client, match.id);
229
229
  }
230
230
  else if (projects.length === 1) {
231
- project = await getProjectDetail(client, projects[0].id);
231
+ githubProject = await getProjectDetail(client, projects[0].id);
232
232
  }
233
233
  else {
234
234
  process.stderr.write("Error: --project is required when multiple projects exist.\n");
@@ -236,8 +236,8 @@ async function runNonInteractive(flags, options) {
236
236
  return;
237
237
  }
238
238
  // Auto-map with smart defaults
239
- const statusField = project.statusFields.find((f) => f.name.toLowerCase() === "status") ??
240
- project.statusFields[0];
239
+ const statusField = githubProject.statusFields.find((f) => f.name.toLowerCase() === "status") ??
240
+ githubProject.statusFields[0];
241
241
  if (!statusField) {
242
242
  process.stderr.write("Error: No status field found on the project.\n");
243
243
  process.exitCode = 1;
@@ -260,7 +260,7 @@ async function runNonInteractive(flags, options) {
260
260
  const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
261
261
  const outputPath = resolve(flags.output ?? "WORKFLOW.md");
262
262
  const workflowMd = generateWorkflowMarkdown({
263
- projectId: project.id,
263
+ projectId: githubProject.id,
264
264
  stateFieldName: statusField.name,
265
265
  mappings,
266
266
  lifecycle: lifecycleConfig,
@@ -269,7 +269,7 @@ async function runNonInteractive(flags, options) {
269
269
  await writeFile(outputPath, workflowMd, "utf8");
270
270
  const ecosystemResult = await writeEcosystem({
271
271
  cwd: process.cwd(),
272
- projectDetail: project,
272
+ projectDetail: githubProject,
273
273
  statusField,
274
274
  runtime: "codex",
275
275
  skipSkills: flags.skipSkills,
@@ -281,7 +281,7 @@ async function runNonInteractive(flags, options) {
281
281
  else {
282
282
  printEcosystemSummary(ecosystemResult, outputPath, {
283
283
  interactive: false,
284
- nextSteps: "Run 'gh-symphony tenant add' to register a tenant.",
284
+ nextSteps: "Run 'gh-symphony project add' to register a project.",
285
285
  });
286
286
  }
287
287
  }
@@ -346,8 +346,8 @@ async function runInteractiveStandalone(_options) {
346
346
  process.exitCode = 1;
347
347
  return;
348
348
  }
349
- const selectedProjectId = await abortIfCancelled(p.select({
350
- 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:",
351
351
  options: projects.map((proj) => ({
352
352
  value: proj.id,
353
353
  label: `${proj.owner.login}/${proj.title}`,
@@ -359,7 +359,7 @@ async function runInteractiveStandalone(_options) {
359
359
  s2d.start("Loading project details...");
360
360
  let projectDetail;
361
361
  try {
362
- projectDetail = await getProjectDetail(client, selectedProjectId);
362
+ projectDetail = await getProjectDetail(client, selectedGithubProjectId);
363
363
  s2d.stop(`Loaded: ${projectDetail.title}`);
364
364
  }
365
365
  catch (error) {
@@ -432,13 +432,14 @@ async function runInteractiveStandalone(_options) {
432
432
  });
433
433
  printEcosystemSummary(ecosystemResult, outputPath, {
434
434
  interactive: true,
435
- nextSteps: "Run 'gh-symphony tenant add' to register a tenant.",
435
+ nextSteps: "Run 'gh-symphony project add' to register a project.",
436
436
  });
437
437
  }
438
438
  export async function writeConfig(configDir, input) {
439
- await saveTenantConfig(configDir, input.tenantId, {
440
- tenantId: input.tenantId,
441
- slug: input.tenantId,
439
+ await saveProjectConfig(configDir, input.projectId, {
440
+ projectId: input.projectId,
441
+ slug: input.projectId,
442
+ displayName: input.project.title,
442
443
  workspaceDir: input.workspaceDir,
443
444
  repositories: input.repos.map((r) => ({
444
445
  owner: r.owner,
@@ -457,20 +458,20 @@ export async function writeConfig(configDir, input) {
457
458
  // Save/update global config
458
459
  const existing = await loadGlobalConfig(configDir);
459
460
  const globalConfig = {
460
- activeTenant: input.tenantId,
461
- tenants: [
462
- ...(existing?.tenants ?? []).filter((t) => t !== input.tenantId),
463
- input.tenantId,
461
+ activeProject: input.projectId,
462
+ projects: [
463
+ ...(existing?.projects ?? []).filter((t) => t !== input.projectId),
464
+ input.projectId,
464
465
  ],
465
466
  };
466
467
  await saveGlobalConfig(configDir, globalConfig);
467
468
  }
468
- export function generateTenantId(projectTitle, uniqueKey) {
469
- const slug = projectTitle
469
+ export function generateProjectId(githubProjectTitle, uniqueKey) {
470
+ const slug = githubProjectTitle
470
471
  .toLowerCase()
471
472
  .replace(/[^a-z0-9]+/g, "-")
472
473
  .replace(/^-|-$/g, "")
473
474
  .slice(0, 32);
474
475
  const suffix = createHash("sha1").update(uniqueKey).digest("hex").slice(0, 8);
475
- return [slug || "tenant", suffix].join("-");
476
+ return [slug || "project", suffix].join("-");
476
477
  }
@@ -2,7 +2,8 @@ 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 { orchestratorLogPath } from "../config.js";
6
+ import { handleMissingManagedProjectConfig, resolveManagedProjectConfig, } from "../project-selection.js";
6
7
  function parseLogsArgs(args) {
7
8
  const parsed = { follow: false };
8
9
  for (let i = 0; i < args.length; i += 1) {
@@ -22,17 +23,15 @@ function parseLogsArgs(args) {
22
23
  parsed.level = args[i + 1];
23
24
  i += 1;
24
25
  }
26
+ if (arg === "--project" || arg === "--project-id") {
27
+ parsed.projectId = args[i + 1];
28
+ i += 1;
29
+ }
25
30
  }
26
31
  return parsed;
27
32
  }
28
33
  const handler = async (args, options) => {
29
34
  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");
33
- process.exitCode = 1;
34
- return;
35
- }
36
35
  // If --run is specified, read that run's events
37
36
  if (parsed.run) {
38
37
  const eventsPath = join(resolve(options.configDir), "orchestrator", "runs", parsed.run, "events.ndjson");
@@ -41,6 +40,8 @@ const handler = async (args, options) => {
41
40
  const lines = content.trim().split("\n").filter(Boolean);
42
41
  for (const line of lines) {
43
42
  const event = JSON.parse(line);
43
+ if (parsed.projectId && event.projectId !== parsed.projectId)
44
+ continue;
44
45
  if (parsed.level && event.level !== parsed.level)
45
46
  continue;
46
47
  if (parsed.issue && event.issueIdentifier !== parsed.issue)
@@ -56,7 +57,15 @@ const handler = async (args, options) => {
56
57
  }
57
58
  // Default: read orchestrator log or scan all events
58
59
  if (parsed.follow) {
59
- const logPath = orchestratorLogPath(options.configDir);
60
+ const projectConfig = await resolveManagedProjectConfig({
61
+ configDir: options.configDir,
62
+ requestedProjectId: parsed.projectId,
63
+ });
64
+ if (!projectConfig) {
65
+ handleMissingManagedProjectConfig();
66
+ return;
67
+ }
68
+ const logPath = orchestratorLogPath(options.configDir, projectConfig.projectId);
60
69
  try {
61
70
  const stream = createReadStream(logPath, { encoding: "utf8" });
62
71
  const rl = createInterface({ input: stream });
@@ -97,6 +106,8 @@ const handler = async (args, options) => {
97
106
  const lines = content.trim().split("\n").filter(Boolean);
98
107
  for (const line of lines) {
99
108
  const event = JSON.parse(line);
109
+ if (parsed.projectId && event.projectId !== parsed.projectId)
110
+ continue;
100
111
  if (parsed.level && event.level !== parsed.level)
101
112
  continue;
102
113
  if (parsed.issue && event.issueIdentifier !== parsed.issue)
@@ -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
+ }