@gh-symphony/cli 0.0.4 → 0.0.6

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
@@ -61,8 +61,8 @@ 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)
64
+ 4. Optionally limit processing to issues assigned to the authenticated user
65
+ 5. Configure the workspace root directory
66
66
  6. Write tenant configuration to `~/.gh-symphony/`
67
67
 
68
68
  ### Tenant Management
@@ -41,7 +41,7 @@ Global Options:
41
41
 
42
42
  Examples:
43
43
  gh-symphony tenant add # Add a tenant (interactive)
44
- gh-symphony tenant add --non-interactive --project <id>
44
+ gh-symphony tenant add --non-interactive --project <id> --workspace-dir <path>
45
45
  gh-symphony tenant list # List all tenants
46
46
  gh-symphony tenant remove <id> # Remove a tenant
47
47
  gh-symphony start # Start orchestrator
@@ -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;
@@ -27,22 +26,9 @@ type WriteConfigInput = {
27
26
  tenantId: 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;
31
+ assignedOnly?: boolean;
46
32
  };
47
33
  export declare function writeConfig(configDir: string, input: WriteConfigInput): Promise<void>;
48
34
  export declare function generateTenantId(projectTitle: 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, saveTenantConfig, } 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";
@@ -436,29 +435,11 @@ async function runInteractiveStandalone(_options) {
436
435
  nextSteps: "Run 'gh-symphony tenant add' to register a tenant.",
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
439
  await saveTenantConfig(configDir, input.tenantId, {
460
440
  tenantId: input.tenantId,
461
441
  slug: input.tenantId,
442
+ workspaceDir: input.workspaceDir,
462
443
  repositories: input.repos.map((r) => ({
463
444
  owner: r.owner,
464
445
  name: r.name,
@@ -469,15 +450,9 @@ export async function writeConfig(configDir, input) {
469
450
  bindingId: input.project.id,
470
451
  settings: {
471
452
  projectId: input.project.id,
453
+ ...(input.assignedOnly ? { assignedOnly: true } : {}),
472
454
  },
473
455
  },
474
- runtime: {
475
- driver: "local",
476
- workspaceRuntimeDir: runtimeDir,
477
- projectRoot: process.cwd(),
478
- workerCommand: input.workerCommand ?? resolveWorkerCommand(),
479
- },
480
- workflowMapping: mappingConfig,
481
456
  });
482
457
  // Save/update global config
483
458
  const existing = await loadGlobalConfig(configDir);
@@ -489,18 +464,6 @@ export async function writeConfig(configDir, input) {
489
464
  ],
490
465
  };
491
466
  await saveGlobalConfig(configDir, globalConfig);
492
- // Generate WORKFLOW.md for tenant-level fallback
493
- const workflowMd = generateWorkflowMarkdown({
494
- projectId: input.project.id,
495
- stateFieldName: input.statusField.name,
496
- mappings: input.mappings,
497
- lifecycle: lifecycleConfig,
498
- runtime: input.agentCommand ?? input.runtime,
499
- pollIntervalMs: input.pollIntervalMs,
500
- concurrency: input.concurrency,
501
- });
502
- const workflowMdPath = join(configDir, "tenants", input.tenantId, "WORKFLOW.md");
503
- await writeFile(workflowMdPath, workflowMd, "utf8");
504
467
  }
505
468
  export function generateTenantId(projectTitle, uniqueKey) {
506
469
  const slug = projectTitle
@@ -151,6 +151,9 @@ const handler = async (args, options) => {
151
151
  return snapshot ?? null;
152
152
  },
153
153
  },
154
+ onRefresh: async () => {
155
+ await service.runOnce({ tenantId });
156
+ },
154
157
  });
155
158
  logLine(green("\u25B2"), `Starting orchestrator for tenant: ${bold(tenantId)}`);
156
159
  logLine(dim("\u00B7"), dim("Press Ctrl+C to stop"));
@@ -0,0 +1,8 @@
1
+ type RefreshRequestOptions = {
2
+ fetchImpl?: typeof fetch;
3
+ timeoutMs?: number;
4
+ env?: NodeJS.ProcessEnv;
5
+ };
6
+ export declare function resolveOrchestratorStatusBaseUrl(env?: NodeJS.ProcessEnv): string;
7
+ export declare function requestOrchestratorRefresh(options?: RefreshRequestOptions): Promise<boolean>;
8
+ export {};
@@ -0,0 +1,21 @@
1
+ export function resolveOrchestratorStatusBaseUrl(env = process.env) {
2
+ const host = env.ORCHESTRATOR_STATUS_HOST ?? "127.0.0.1";
3
+ const port = env.ORCHESTRATOR_STATUS_PORT ?? "4680";
4
+ const urlHost = host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
5
+ return `http://${urlHost}:${port}`;
6
+ }
7
+ export async function requestOrchestratorRefresh(options = {}) {
8
+ const fetchImpl = options.fetchImpl ?? fetch;
9
+ const timeoutMs = options.timeoutMs ?? 5_000;
10
+ const signal = AbortSignal.timeout(timeoutMs);
11
+ try {
12
+ const response = await fetchImpl(`${resolveOrchestratorStatusBaseUrl(options.env)}/api/v1/refresh`, {
13
+ method: "POST",
14
+ signal,
15
+ });
16
+ return response.ok;
17
+ }
18
+ catch {
19
+ return false;
20
+ }
21
+ }
@@ -4,6 +4,8 @@ import { resolveRuntimeRoot, resolveTenantConfig, syncTenantToRuntime, } from ".
4
4
  import { bold, dim, green, red, yellow, cyan, stripAnsi } from "../ansi.js";
5
5
  import { clearScreen, showCursor, hideCursor } from "../ansi.js";
6
6
  import { renderDashboard } from "../dashboard/renderer.js";
7
+ import { requestOrchestratorRefresh } from "./status-refresh.js";
8
+ const WATCH_REFRESH_TIMEOUT_MS = 1_500;
7
9
  function healthIcon(health) {
8
10
  switch (health) {
9
11
  case "idle":
@@ -166,7 +168,11 @@ const handler = async (args, options) => {
166
168
  if (parsed.watch) {
167
169
  const isTTY = process.stdout.isTTY === true;
168
170
  let terminalWidth = process.stdout.columns ?? 115;
171
+ let runPromise = null;
169
172
  const run = async () => {
173
+ await requestOrchestratorRefresh({
174
+ timeoutMs: WATCH_REFRESH_TIMEOUT_MS,
175
+ });
170
176
  const snapshots = await readAllStatusSnapshots(runtimeRoot);
171
177
  if (options.json || !isTTY) {
172
178
  process.stdout.write(JSON.stringify(snapshots, null, 2) + "\n");
@@ -180,11 +186,20 @@ const handler = async (args, options) => {
180
186
  "\n");
181
187
  }
182
188
  };
189
+ const tick = () => {
190
+ if (runPromise) {
191
+ return;
192
+ }
193
+ runPromise = run().finally(() => {
194
+ runPromise = null;
195
+ });
196
+ };
183
197
  if (isTTY) {
184
198
  process.stdout.write(hideCursor());
185
199
  }
186
- await run();
187
- const interval = setInterval(() => void run(), 2000);
200
+ tick();
201
+ await runPromise;
202
+ const interval = setInterval(tick, 2000);
188
203
  process.on("SIGWINCH", () => {
189
204
  terminalWidth = process.stdout.columns ?? terminalWidth;
190
205
  });
@@ -1,7 +1,6 @@
1
1
  import * as p from "@clack/prompts";
2
2
  import { createClient, validateToken, checkRequiredScopes, listUserProjects, getProjectDetail, GitHubScopeError, } from "../github/client.js";
3
3
  import { ensureGhAuth, getGhToken, GhAuthError } from "../github/gh-auth.js";
4
- import { inferAllStateRoles, toWorkflowLifecycleConfig, validateStateMapping, } from "../mapping/smart-defaults.js";
5
4
  import { loadGlobalConfig, saveGlobalConfig, loadTenantConfig, tenantConfigDir, } from "../config.js";
6
5
  import { writeConfig, generateTenantId, abortIfCancelled } from "./init.js";
7
6
  // ── Scope error display ───────────────────────────────────────────────────────
@@ -17,7 +16,7 @@ function displayScopeError(error, retryCommand) {
17
16
  p.note(`gh auth refresh --scopes ${scopeArg}\n\nThen re-run: ${retryCommand}`, "Fix missing scope");
18
17
  }
19
18
  function parseTenantAddFlags(args) {
20
- const flags = { nonInteractive: false };
19
+ const flags = { nonInteractive: false, assignedOnly: false };
21
20
  for (let i = 0; i < args.length; i += 1) {
22
21
  const arg = args[i];
23
22
  const next = args[i + 1];
@@ -29,10 +28,13 @@ function parseTenantAddFlags(args) {
29
28
  flags.project = next;
30
29
  i += 1;
31
30
  break;
32
- case "--runtime":
33
- flags.runtime = next;
31
+ case "--workspace-dir":
32
+ flags.workspaceDir = next;
34
33
  i += 1;
35
34
  break;
35
+ case "--assigned-only":
36
+ flags.assignedOnly = true;
37
+ break;
36
38
  }
37
39
  }
38
40
  return flags;
@@ -112,37 +114,14 @@ async function tenantAddNonInteractive(flags, options) {
112
114
  process.exitCode = 1;
113
115
  return;
114
116
  }
115
- // Auto-map with smart defaults
116
- const statusField = project.statusFields.find((f) => f.name.toLowerCase() === "status") ??
117
- project.statusFields[0];
118
- if (!statusField) {
119
- process.stderr.write("Error: No status field found on the project.\n");
120
- process.exitCode = 1;
121
- return;
122
- }
123
- const columnNames = statusField.options.map((o) => o.name);
124
- const inferred = inferAllStateRoles(columnNames);
125
- const mappings = {};
126
- for (const mapping of inferred) {
127
- if (mapping.role) {
128
- mappings[mapping.columnName] = { role: mapping.role };
129
- }
130
- }
131
- const validation = validateStateMapping(mappings);
132
- if (!validation.valid) {
133
- process.stderr.write(`Error: Cannot auto-map columns. ${validation.errors.join("; ")}\nRun without --non-interactive for manual mapping.\n`);
134
- process.exitCode = 1;
135
- return;
136
- }
137
- const runtime = flags.runtime ?? "codex";
138
117
  const tenantId = generateTenantId(project.title, project.id);
118
+ const workspaceDir = flags.workspaceDir ?? `${options.configDir}/workspaces`;
139
119
  await writeConfig(options.configDir, {
140
120
  tenantId,
141
121
  project,
142
122
  repos: project.linkedRepositories,
143
- statusField,
144
- mappings,
145
- runtime,
123
+ workspaceDir,
124
+ assignedOnly: flags.assignedOnly,
146
125
  });
147
126
  if (options.json) {
148
127
  process.stdout.write(JSON.stringify({ tenantId, status: "created" }) + "\n");
@@ -228,7 +207,7 @@ async function tenantAddInteractive(options) {
228
207
  return;
229
208
  }
230
209
  const selectedProjectId = await abortIfCancelled(p.select({
231
- message: "Step 1/3 — Select a GitHub Project:",
210
+ message: "Step 1/4 — Select a GitHub Project:",
232
211
  options: projects.map((proj) => ({
233
212
  value: proj.id,
234
213
  label: `${proj.owner.login}/${proj.title}`,
@@ -249,68 +228,42 @@ async function tenantAddInteractive(options) {
249
228
  process.exitCode = 1;
250
229
  return;
251
230
  }
252
- // ── Step 3: Repository selection ────────────────────────────────────────────
231
+ // ── Step 2: Repository selection ────────────────────────────────────────────
253
232
  if (projectDetail.linkedRepositories.length === 0) {
254
233
  p.log.warn("No linked repositories found in this project. Add issues from repositories to the project first.");
255
234
  process.exitCode = 1;
256
235
  return;
257
236
  }
258
237
  const selectedRepos = await abortIfCancelled(p.multiselect({
259
- message: "Step 2/3 — Select repositories to orchestrate:",
238
+ message: "Step 2/4 — Select repositories to orchestrate:",
260
239
  options: projectDetail.linkedRepositories.map((repo) => ({
261
240
  value: repo,
262
241
  label: `${repo.owner}/${repo.name}`,
263
242
  })),
264
243
  required: true,
265
244
  }));
266
- // ── Step 4: Status column auto-detection ─────────────────────────────────────
267
- const statusField = projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ??
268
- projectDetail.statusFields[0];
269
- if (!statusField) {
270
- p.log.error("No status field found on the project. The project needs a single-select 'Status' field.");
271
- process.exitCode = 1;
272
- return;
273
- }
274
- const columnNames = statusField.options.map((o) => o.name);
275
- const inferred = inferAllStateRoles(columnNames);
276
- const mappings = {};
277
- for (const mapping of inferred) {
278
- if (mapping.role) {
279
- mappings[mapping.columnName] = { role: mapping.role };
280
- }
281
- }
282
- const validation = validateStateMapping(mappings);
283
- if (!validation.valid) {
284
- p.log.error(`Cannot auto-map status columns: ${validation.errors.join("; ")}\nRun 'gh-symphony init' to manually configure WORKFLOW.md.`);
285
- process.exitCode = 1;
286
- return;
287
- }
288
- const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
289
- p.log.info(`Auto-detected workflow: Active=[${lifecycleConfig.activeStates.join(", ")}] Terminal=[${lifecycleConfig.terminalStates.join(", ")}]`);
290
- // ── Step 4: Runtime selection ────────────────────────────────────────────────
291
- const runtime = await abortIfCancelled(p.select({
292
- message: "Step 3/3 — Select AI runtime:",
293
- options: [
294
- { value: "codex", label: "OpenAI Codex", hint: "recommended" },
295
- { value: "claude-code", label: "Claude Code" },
296
- { value: "custom", label: "Custom command" },
297
- ],
245
+ // ── Step 3: Assignment filter ────────────────────────────────────────────────
246
+ const assignedOnly = await abortIfCancelled(p.confirm({
247
+ message: "Step 3/4 — Only process issues assigned to the authenticated GitHub user?",
248
+ initialValue: false,
249
+ }));
250
+ const workspaceDir = await abortIfCancelled(p.text({
251
+ message: "Step 4/4 — Workspace root directory:",
252
+ placeholder: `${options.configDir}/workspaces`,
253
+ defaultValue: `${options.configDir}/workspaces`,
254
+ validate(value) {
255
+ return value.trim().length > 0
256
+ ? undefined
257
+ : "Workspace directory is required.";
258
+ },
298
259
  }));
299
- let agentCommand;
300
- if (runtime === "custom") {
301
- agentCommand = await abortIfCancelled(p.text({
302
- message: "Custom agent command:",
303
- placeholder: "bash -lc my-agent",
304
- }));
305
- }
306
260
  // ── Confirmation ─────────────────────────────────────────────────────────────
307
261
  p.note([
308
262
  `User: ${login}`,
309
263
  `Project: ${projectDetail.title}`,
310
264
  `Repos: ${selectedRepos.map((r) => `${r.owner}/${r.name}`).join(", ")}`,
311
- `Runtime: ${runtime}`,
312
- `Active: ${lifecycleConfig.activeStates.join(", ")}`,
313
- `Terminal: ${lifecycleConfig.terminalStates.join(", ")}`,
265
+ `Assigned: ${assignedOnly ? `Only issues assigned to ${login}` : "All project issues"}`,
266
+ `Workspace: ${workspaceDir}`,
314
267
  ].join("\n"), "Configuration Summary");
315
268
  const confirmed = await abortIfCancelled(p.confirm({ message: "Apply this configuration?" }));
316
269
  if (!confirmed) {
@@ -327,14 +280,8 @@ async function tenantAddInteractive(options) {
327
280
  tenantId,
328
281
  project: projectDetail,
329
282
  repos: selectedRepos,
330
- statusField: {
331
- id: statusField.id,
332
- name: statusField.name,
333
- options: statusField.options,
334
- },
335
- mappings,
336
- runtime,
337
- agentCommand,
283
+ workspaceDir,
284
+ assignedOnly,
338
285
  });
339
286
  s6.stop("Configuration saved.");
340
287
  }
@@ -344,7 +291,6 @@ async function tenantAddInteractive(options) {
344
291
  process.exitCode = 1;
345
292
  return;
346
293
  }
347
- p.log.info(`WORKFLOW.md generated at ${tenantId}/WORKFLOW.md — edit it to customize your team policy.`);
348
294
  p.outro(`Tenant "${tenantId}" created!\n Run 'gh-symphony start' to begin orchestration.`);
349
295
  }
350
296
  // ── tenant list ───────────────────────────────────────────────────────────────
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { OrchestratorTenantConfig, WorkflowLifecycleConfig } from "@gh-symphony/core";
1
+ import type { OrchestratorTenantConfig } from "@gh-symphony/core";
2
2
  export declare const DEFAULT_CONFIG_DIR: string;
3
3
  export declare const CONFIG_FILE = "config.json";
4
4
  export declare const DAEMON_PID_FILE = "daemon.pid";
@@ -7,24 +7,24 @@ export type CliGlobalConfig = {
7
7
  activeTenant: string | null;
8
8
  tenants: string[];
9
9
  };
10
- export type CliTenantConfig = OrchestratorTenantConfig & {
11
- workflowMapping?: WorkflowStateConfig;
10
+ export type CliTenantTrackerSettings = Record<string, string | boolean> & {
11
+ projectId?: string;
12
+ assignedOnly?: boolean;
13
+ };
14
+ export type CliTenantConfig = Omit<OrchestratorTenantConfig, "tracker"> & {
15
+ tracker: Omit<OrchestratorTenantConfig["tracker"], "settings"> & {
16
+ settings?: CliTenantTrackerSettings;
17
+ };
12
18
  };
13
19
  export type StateRole = "active" | "wait" | "terminal";
14
20
  export type StateMapping = {
15
21
  role: StateRole;
16
22
  goal?: string;
17
23
  };
18
- export type WorkflowStateConfig = {
19
- stateFieldName: string;
20
- mappings: Record<string, StateMapping>;
21
- lifecycle: WorkflowLifecycleConfig;
22
- };
23
24
  export declare function resolveConfigDir(override?: string): string;
24
25
  export declare function configFilePath(configDir: string): string;
25
26
  export declare function tenantConfigDir(configDir: string, tenantId: string): string;
26
27
  export declare function tenantConfigPath(configDir: string, tenantId: string): string;
27
- export declare function workflowMappingPath(configDir: string, tenantId: string): string;
28
28
  export declare function daemonPidPath(configDir: string): string;
29
29
  export declare function logsDir(configDir: string): string;
30
30
  export declare function orchestratorLogPath(configDir: string): string;
@@ -32,6 +32,4 @@ export declare function loadGlobalConfig(configDir: string): Promise<CliGlobalCo
32
32
  export declare function saveGlobalConfig(configDir: string, config: CliGlobalConfig): Promise<void>;
33
33
  export declare function loadTenantConfig(configDir: string, tenantId: string): Promise<CliTenantConfig | null>;
34
34
  export declare function saveTenantConfig(configDir: string, tenantId: string, config: CliTenantConfig): Promise<void>;
35
- export declare function loadWorkflowMapping(configDir: string, tenantId: string): Promise<WorkflowStateConfig | null>;
36
- export declare function saveWorkflowMapping(configDir: string, tenantId: string, mapping: WorkflowStateConfig): Promise<void>;
37
35
  export declare function loadActiveTenantConfig(configDir: string): Promise<CliTenantConfig | null>;
package/dist/config.js CHANGED
@@ -17,9 +17,6 @@ export function tenantConfigDir(configDir, tenantId) {
17
17
  export function tenantConfigPath(configDir, tenantId) {
18
18
  return join(tenantConfigDir(configDir, tenantId), "tenant.json");
19
19
  }
20
- export function workflowMappingPath(configDir, tenantId) {
21
- return join(tenantConfigDir(configDir, tenantId), "workflow-mapping.json");
22
- }
23
20
  export function daemonPidPath(configDir) {
24
21
  return join(configDir, DAEMON_PID_FILE);
25
22
  }
@@ -41,12 +38,6 @@ export async function loadTenantConfig(configDir, tenantId) {
41
38
  export async function saveTenantConfig(configDir, tenantId, config) {
42
39
  await writeJsonFile(tenantConfigPath(configDir, tenantId), config);
43
40
  }
44
- export async function loadWorkflowMapping(configDir, tenantId) {
45
- return readJsonFile(workflowMappingPath(configDir, tenantId));
46
- }
47
- export async function saveWorkflowMapping(configDir, tenantId, mapping) {
48
- await writeJsonFile(workflowMappingPath(configDir, tenantId), mapping);
49
- }
50
41
  export async function loadActiveTenantConfig(configDir) {
51
42
  const global = await loadGlobalConfig(configDir);
52
43
  if (!global?.activeTenant) {
@@ -1,7 +1,7 @@
1
1
  // ── Dashboard Renderer (Elixir-parity) ──────────────────────────────────────
2
2
  import { bold, dim, green, red, yellow, cyan, magenta, blue, stripAnsi, } from "../ansi.js";
3
3
  // ── Column widths (from Elixir spec) ─────────────────────────────────────────
4
- const COL_ID = 8;
4
+ const COL_ID = 24;
5
5
  const COL_STAGE = 14;
6
6
  const COL_PID = 8;
7
7
  const COL_AGE_TURN = 12;
@@ -152,7 +152,7 @@ function tableHeaderRow(c) {
152
152
  function activeRunRow(run, now, evtWidth, c) {
153
153
  const dot = statusDot(run, c);
154
154
  const id = pad(run.issueIdentifier, COL_ID);
155
- const stage = pad(run.issueState, COL_STAGE);
155
+ const stage = pad(run.issueState || "\u2014", COL_STAGE);
156
156
  const pid = pad(run.processId != null ? String(run.processId) : "\u2014", COL_PID);
157
157
  const age = fmtAge(run.startedAt, now);
158
158
  const turn = run.turnCount ?? 0;
@@ -31,7 +31,7 @@ export function checkGhAuthenticated(opts) {
31
31
  if ((result.status ?? 1) !== 0) {
32
32
  return { authenticated: false };
33
33
  }
34
- const login = parseLogin((result.stderr ?? "").toString());
34
+ const login = parseLogin((result.stdout ?? "").toString());
35
35
  return { authenticated: true, login };
36
36
  }
37
37
  export function checkGhScopes(opts) {
@@ -40,7 +40,7 @@ export function checkGhScopes(opts) {
40
40
  encoding: "utf8",
41
41
  stdio: ["pipe", "pipe", "pipe"],
42
42
  });
43
- const output = (result.stderr ?? "").toString();
43
+ const output = (result.stdout ?? "").toString();
44
44
  const scopes = parseScopes(output);
45
45
  if (scopes.length === 0) {
46
46
  return { valid: true, missing: [], scopes: [] };
@@ -1,4 +1,4 @@
1
- import { copyFile, mkdir, writeFile } from "node:fs/promises";
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { loadGlobalConfig, loadTenantConfig, } from "./config.js";
4
4
  export function resolveRuntimeRoot(configDir) {
@@ -22,20 +22,5 @@ export async function syncTenantToRuntime(configDir, tenantConfig) {
22
22
  const configPath = orchestratorTenantConfigPath(runtimeRoot, tenantConfig.tenantId);
23
23
  await mkdir(dirname(configPath), { recursive: true });
24
24
  await writeFile(configPath, JSON.stringify(tenantConfig, null, 2) + "\n");
25
- // Copy tenant WORKFLOW.md to runtime if it exists
26
- const workflowSrc = join(configDir, "tenants", tenantConfig.tenantId, "WORKFLOW.md");
27
- const workflowDst = join(dirname(configPath), "WORKFLOW.md");
28
- try {
29
- await copyFile(workflowSrc, workflowDst);
30
- }
31
- catch (error) {
32
- // ENOENT is expected for tenants created before WORKFLOW.md scaffolding
33
- if (!(error &&
34
- typeof error === "object" &&
35
- "code" in error &&
36
- error.code === "ENOENT")) {
37
- throw error;
38
- }
39
- }
40
25
  return runtimeRoot;
41
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gh-symphony/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "license": "MIT",
5
5
  "author": "hojinzs",
6
6
  "description": "Interactive CLI for GitHub Symphony orchestration",
@@ -36,10 +36,10 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@clack/prompts": "^0.9.1",
39
- "@gh-symphony/core": "0.0.3",
40
- "@gh-symphony/orchestrator": "0.0.3",
41
- "@gh-symphony/tracker-github": "0.0.3",
42
- "@gh-symphony/worker": "0.0.3"
39
+ "@gh-symphony/core": "0.0.6",
40
+ "@gh-symphony/orchestrator": "0.0.6",
41
+ "@gh-symphony/tracker-github": "0.0.6",
42
+ "@gh-symphony/worker": "0.0.6"
43
43
  },
44
44
  "scripts": {
45
45
  "build": "tsc -p tsconfig.json",