@gh-symphony/cli 0.0.1 → 0.0.2

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 (59) hide show
  1. package/dist/ansi.d.ts +15 -0
  2. package/dist/ansi.js +53 -0
  3. package/dist/commands/config-cmd.js +11 -27
  4. package/dist/commands/help.js +14 -6
  5. package/dist/commands/init.d.ts +30 -7
  6. package/dist/commands/init.js +421 -284
  7. package/dist/commands/logs.js +4 -4
  8. package/dist/commands/project.js +34 -34
  9. package/dist/commands/recover.js +14 -14
  10. package/dist/commands/repo.js +13 -13
  11. package/dist/commands/run.js +16 -16
  12. package/dist/commands/start.js +61 -37
  13. package/dist/commands/status.js +60 -63
  14. package/dist/commands/tenant.d.ts +3 -0
  15. package/dist/commands/tenant.js +402 -0
  16. package/dist/config.d.ts +20 -19
  17. package/dist/config.js +17 -17
  18. package/dist/context/context-types.d.ts +36 -0
  19. package/dist/context/context-types.js +1 -0
  20. package/dist/context/generate-context-yaml.d.ts +15 -0
  21. package/dist/context/generate-context-yaml.js +129 -0
  22. package/dist/dashboard/renderer.d.ts +9 -0
  23. package/dist/dashboard/renderer.js +220 -0
  24. package/dist/detection/environment-detector.d.ts +11 -0
  25. package/dist/detection/environment-detector.js +140 -0
  26. package/dist/github/client.d.ts +11 -0
  27. package/dist/github/client.js +59 -11
  28. package/dist/github/gh-auth.d.ts +34 -0
  29. package/dist/github/gh-auth.js +110 -0
  30. package/dist/index.js +1 -0
  31. package/dist/mapping/smart-defaults.d.ts +9 -25
  32. package/dist/mapping/smart-defaults.js +52 -125
  33. package/dist/orchestrator-runtime.d.ts +4 -4
  34. package/dist/orchestrator-runtime.js +27 -12
  35. package/dist/skills/skill-writer.d.ts +14 -0
  36. package/dist/skills/skill-writer.js +62 -0
  37. package/dist/skills/templates/commit.d.ts +2 -0
  38. package/dist/skills/templates/commit.js +45 -0
  39. package/dist/skills/templates/document.d.ts +7 -0
  40. package/dist/skills/templates/document.js +16 -0
  41. package/dist/skills/templates/gh-project.d.ts +2 -0
  42. package/dist/skills/templates/gh-project.js +88 -0
  43. package/dist/skills/templates/gh-symphony.d.ts +2 -0
  44. package/dist/skills/templates/gh-symphony.js +125 -0
  45. package/dist/skills/templates/index.d.ts +8 -0
  46. package/dist/skills/templates/index.js +28 -0
  47. package/dist/skills/templates/land.d.ts +2 -0
  48. package/dist/skills/templates/land.js +59 -0
  49. package/dist/skills/templates/pull.d.ts +2 -0
  50. package/dist/skills/templates/pull.js +41 -0
  51. package/dist/skills/templates/push.d.ts +2 -0
  52. package/dist/skills/templates/push.js +36 -0
  53. package/dist/skills/types.d.ts +23 -0
  54. package/dist/skills/types.js +1 -0
  55. package/dist/workflow/generate-reference-workflow.d.ts +9 -0
  56. package/dist/workflow/generate-reference-workflow.js +261 -0
  57. package/dist/workflow/generate-workflow-md.d.ts +12 -0
  58. package/dist/workflow/generate-workflow-md.js +134 -0
  59. package/package.json +5 -4
@@ -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 { loadActiveWorkspaceConfig, orchestratorLogPath } from "../config.js";
5
+ import { loadActiveTenantConfig, 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) {
@@ -27,9 +27,9 @@ function parseLogsArgs(args) {
27
27
  }
28
28
  const handler = async (args, options) => {
29
29
  const parsed = parseLogsArgs(args);
30
- const wsConfig = await loadActiveWorkspaceConfig(options.configDir);
31
- if (!wsConfig) {
32
- process.stderr.write("No workspace configured. Run 'gh-symphony init' first.\n");
30
+ const tenantConfig = await loadActiveTenantConfig(options.configDir);
31
+ if (!tenantConfig) {
32
+ process.stderr.write("No tenant configured. Run 'gh-symphony init' first.\n");
33
33
  process.exitCode = 1;
34
34
  return;
35
35
  }
@@ -1,5 +1,5 @@
1
1
  import * as p from "@clack/prompts";
2
- import { loadGlobalConfig, saveGlobalConfig, loadWorkspaceConfig, } from "../config.js";
2
+ import { loadGlobalConfig, saveGlobalConfig, loadTenantConfig, } from "../config.js";
3
3
  const handler = async (args, options) => {
4
4
  const [subcommand] = args;
5
5
  switch (subcommand) {
@@ -21,81 +21,81 @@ export default handler;
21
21
  // ── 6.1: project list ────────────────────────────────────────────────────────
22
22
  async function projectList(options) {
23
23
  const global = await loadGlobalConfig(options.configDir);
24
- if (!global || global.workspaces.length === 0) {
25
- process.stdout.write("No workspaces configured. Run 'gh-symphony init'.\n");
24
+ if (!global || global.tenants.length === 0) {
25
+ process.stdout.write("No tenants configured. Run 'gh-symphony init'.\n");
26
26
  return;
27
27
  }
28
28
  if (options.json) {
29
29
  const configs = [];
30
- for (const wsId of global.workspaces) {
31
- const ws = await loadWorkspaceConfig(options.configDir, wsId);
30
+ for (const tId of global.tenants) {
31
+ const t = await loadTenantConfig(options.configDir, tId);
32
32
  configs.push({
33
- id: wsId,
34
- active: wsId === global.activeWorkspace,
35
- repos: ws?.repositories.length ?? 0,
33
+ id: tId,
34
+ active: tId === global.activeTenant,
35
+ repos: t?.repositories.length ?? 0,
36
36
  });
37
37
  }
38
38
  process.stdout.write(JSON.stringify(configs, null, 2) + "\n");
39
39
  return;
40
40
  }
41
- process.stdout.write("Workspaces:\n\n");
42
- for (const wsId of global.workspaces) {
43
- const ws = await loadWorkspaceConfig(options.configDir, wsId);
44
- const active = wsId === global.activeWorkspace ? " (active)" : "";
45
- const repos = ws?.repositories.length ?? 0;
46
- process.stdout.write(` ${wsId}${active} — ${repos} repo${repos === 1 ? "" : "s"}\n`);
41
+ process.stdout.write("Tenants:\n\n");
42
+ for (const tId of global.tenants) {
43
+ const t = await loadTenantConfig(options.configDir, tId);
44
+ const active = tId === global.activeTenant ? " (active)" : "";
45
+ const repos = t?.repositories.length ?? 0;
46
+ process.stdout.write(` ${tId}${active} — ${repos} repo${repos === 1 ? "" : "s"}\n`);
47
47
  }
48
48
  }
49
49
  // ── 6.2: project switch ──────────────────────────────────────────────────────
50
50
  async function projectSwitch(options) {
51
51
  const global = await loadGlobalConfig(options.configDir);
52
- if (!global || global.workspaces.length === 0) {
53
- process.stderr.write("No workspaces configured. Run 'gh-symphony init'.\n");
52
+ if (!global || global.tenants.length === 0) {
53
+ process.stderr.write("No tenants configured. Run 'gh-symphony init'.\n");
54
54
  process.exitCode = 1;
55
55
  return;
56
56
  }
57
- if (global.workspaces.length === 1) {
58
- process.stdout.write(`Only one workspace exists: ${global.workspaces[0]}\n`);
57
+ if (global.tenants.length === 1) {
58
+ process.stdout.write(`Only one tenant exists: ${global.tenants[0]}\n`);
59
59
  return;
60
60
  }
61
61
  const selected = await p.select({
62
- message: "Select workspace to activate:",
63
- options: global.workspaces.map((wsId) => ({
64
- value: wsId,
65
- label: wsId,
66
- hint: wsId === global.activeWorkspace ? "current" : undefined,
62
+ message: "Select tenant to activate:",
63
+ options: global.tenants.map((tId) => ({
64
+ value: tId,
65
+ label: tId,
66
+ hint: tId === global.activeTenant ? "current" : undefined,
67
67
  })),
68
68
  });
69
69
  if (p.isCancel(selected)) {
70
70
  p.cancel("Cancelled.");
71
71
  return;
72
72
  }
73
- global.activeWorkspace = selected;
73
+ global.activeTenant = selected;
74
74
  await saveGlobalConfig(options.configDir, global);
75
- process.stdout.write(`Switched to workspace: ${selected}\n`);
75
+ process.stdout.write(`Switched to tenant: ${selected}\n`);
76
76
  }
77
77
  // ── 6.3: project status ──────────────────────────────────────────────────────
78
78
  async function projectStatus(options) {
79
79
  const global = await loadGlobalConfig(options.configDir);
80
- if (!global?.activeWorkspace) {
81
- process.stderr.write("No active workspace.\n");
80
+ if (!global?.activeTenant) {
81
+ process.stderr.write("No active tenant.\n");
82
82
  process.exitCode = 1;
83
83
  return;
84
84
  }
85
- const ws = await loadWorkspaceConfig(options.configDir, global.activeWorkspace);
86
- if (!ws) {
87
- process.stderr.write(`Workspace config missing: ${global.activeWorkspace}\n`);
85
+ const t = await loadTenantConfig(options.configDir, global.activeTenant);
86
+ if (!t) {
87
+ process.stderr.write(`Tenant config missing: ${global.activeTenant}\n`);
88
88
  process.exitCode = 1;
89
89
  return;
90
90
  }
91
91
  if (options.json) {
92
- process.stdout.write(JSON.stringify(ws, null, 2) + "\n");
92
+ process.stdout.write(JSON.stringify(t, null, 2) + "\n");
93
93
  return;
94
94
  }
95
- process.stdout.write(`Workspace: ${ws.workspaceId}\n`);
96
- process.stdout.write(`Tracker: ${ws.tracker.adapter} (${ws.tracker.bindingId})\n`);
95
+ process.stdout.write(`Tenant: ${t.tenantId}\n`);
96
+ process.stdout.write(`Tracker: ${t.tracker.adapter} (${t.tracker.bindingId})\n`);
97
97
  process.stdout.write(`Repositories:\n`);
98
- for (const repo of ws.repositories) {
98
+ for (const repo of t.repositories) {
99
99
  process.stdout.write(` - ${repo.owner}/${repo.name}\n`);
100
100
  }
101
101
  }
@@ -1,7 +1,7 @@
1
1
  import { readFile, readdir } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { runCli as orchestratorRunCli } from "@hojinzs/gh-symphony-orchestrator";
4
- import { resolveRuntimeRoot, resolveWorkspaceConfig, syncWorkspaceToRuntime, } from "../orchestrator-runtime.js";
3
+ import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
4
+ import { resolveRuntimeRoot, resolveTenantConfig, syncTenantToRuntime, } from "../orchestrator-runtime.js";
5
5
  function parseRecoverArgs(args) {
6
6
  const parsed = { dryRun: false };
7
7
  for (let i = 0; i < args.length; i += 1) {
@@ -9,8 +9,8 @@ function parseRecoverArgs(args) {
9
9
  if (arg === "--dry-run") {
10
10
  parsed.dryRun = true;
11
11
  }
12
- if (arg === "--workspace" || arg === "--workspace-id") {
13
- parsed.workspaceId = args[i + 1];
12
+ if (arg === "--tenant" || arg === "--tenant-id") {
13
+ parsed.tenantId = args[i + 1];
14
14
  i += 1;
15
15
  }
16
16
  }
@@ -18,18 +18,18 @@ function parseRecoverArgs(args) {
18
18
  }
19
19
  const handler = async (args, options) => {
20
20
  const parsed = parseRecoverArgs(args);
21
- const wsConfig = await resolveWorkspaceConfig(options.configDir, parsed.workspaceId);
22
- if (!wsConfig) {
23
- process.stderr.write("No workspace configured. Run 'gh-symphony init' first.\n");
21
+ const tenantConfig = await resolveTenantConfig(options.configDir, parsed.tenantId);
22
+ if (!tenantConfig) {
23
+ process.stderr.write("No tenant configured. Run 'gh-symphony init' first.\n");
24
24
  process.exitCode = 1;
25
25
  return;
26
26
  }
27
27
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
28
- const workspaceId = wsConfig.workspaceId;
29
- await syncWorkspaceToRuntime(options.configDir, wsConfig);
28
+ const tenantId = tenantConfig.tenantId;
29
+ await syncTenantToRuntime(options.configDir, tenantConfig);
30
30
  if (parsed.dryRun) {
31
31
  process.stdout.write("Dry run — scanning for stalled runs...\n");
32
- const candidates = await listRecoverCandidates(runtimeRoot, workspaceId);
32
+ const candidates = await listRecoverCandidates(runtimeRoot, tenantId);
33
33
  if (options.json) {
34
34
  process.stdout.write(JSON.stringify(candidates, null, 2) + "\n");
35
35
  return;
@@ -48,12 +48,12 @@ const handler = async (args, options) => {
48
48
  "recover",
49
49
  "--runtime-root",
50
50
  runtimeRoot,
51
- "--workspace-id",
52
- workspaceId,
51
+ "--tenant-id",
52
+ tenantId,
53
53
  ]);
54
54
  };
55
55
  export default handler;
56
- async function listRecoverCandidates(runtimeRoot, workspaceId) {
56
+ async function listRecoverCandidates(runtimeRoot, tenantId) {
57
57
  const runsDir = join(runtimeRoot, "orchestrator", "runs");
58
58
  const candidates = [];
59
59
  let entries = [];
@@ -68,7 +68,7 @@ async function listRecoverCandidates(runtimeRoot, workspaceId) {
68
68
  try {
69
69
  const raw = await readFile(runPath, "utf8");
70
70
  const run = JSON.parse(raw);
71
- if (run.workspaceId !== workspaceId) {
71
+ if (run.tenantId !== tenantId) {
72
72
  continue;
73
73
  }
74
74
  const reason = detectRecoveryReason(run);
@@ -1,4 +1,4 @@
1
- import { loadActiveWorkspaceConfig, loadGlobalConfig, saveWorkspaceConfig, } from "../config.js";
1
+ import { loadActiveTenantConfig, loadGlobalConfig, saveTenantConfig, } from "../config.js";
2
2
  const handler = async (args, options) => {
3
3
  const [subcommand, ...rest] = args;
4
4
  switch (subcommand) {
@@ -19,9 +19,9 @@ const handler = async (args, options) => {
19
19
  export default handler;
20
20
  // ── 6.4: repo list / add / remove ────────────────────────────────────────────
21
21
  async function repoList(options) {
22
- const ws = await loadActiveWorkspaceConfig(options.configDir);
22
+ const ws = await loadActiveTenantConfig(options.configDir);
23
23
  if (!ws) {
24
- process.stderr.write("No workspace configured.\n");
24
+ process.stderr.write("No tenant configured.\n");
25
25
  process.exitCode = 1;
26
26
  return;
27
27
  }
@@ -42,14 +42,14 @@ async function repoAdd(args, options) {
42
42
  return;
43
43
  }
44
44
  const global = await loadGlobalConfig(options.configDir);
45
- if (!global?.activeWorkspace) {
46
- process.stderr.write("No active workspace.\n");
45
+ if (!global?.activeTenant) {
46
+ process.stderr.write("No active tenant.\n");
47
47
  process.exitCode = 1;
48
48
  return;
49
49
  }
50
- const ws = await loadActiveWorkspaceConfig(options.configDir);
50
+ const ws = await loadActiveTenantConfig(options.configDir);
51
51
  if (!ws) {
52
- process.stderr.write("Workspace config missing.\n");
52
+ process.stderr.write("Tenant config missing.\n");
53
53
  process.exitCode = 1;
54
54
  return;
55
55
  }
@@ -68,7 +68,7 @@ async function repoAdd(args, options) {
68
68
  name,
69
69
  cloneUrl: `https://github.com/${owner}/${name}.git`,
70
70
  });
71
- await saveWorkspaceConfig(options.configDir, global.activeWorkspace, ws);
71
+ await saveTenantConfig(options.configDir, global.activeTenant, ws);
72
72
  process.stdout.write(`Added repository: ${repoSpec}\n`);
73
73
  }
74
74
  async function repoRemove(args, options) {
@@ -79,14 +79,14 @@ async function repoRemove(args, options) {
79
79
  return;
80
80
  }
81
81
  const global = await loadGlobalConfig(options.configDir);
82
- if (!global?.activeWorkspace) {
83
- process.stderr.write("No active workspace.\n");
82
+ if (!global?.activeTenant) {
83
+ process.stderr.write("No active tenant.\n");
84
84
  process.exitCode = 1;
85
85
  return;
86
86
  }
87
- const ws = await loadActiveWorkspaceConfig(options.configDir);
87
+ const ws = await loadActiveTenantConfig(options.configDir);
88
88
  if (!ws) {
89
- process.stderr.write("Workspace config missing.\n");
89
+ process.stderr.write("Tenant config missing.\n");
90
90
  process.exitCode = 1;
91
91
  return;
92
92
  }
@@ -98,6 +98,6 @@ async function repoRemove(args, options) {
98
98
  return;
99
99
  }
100
100
  ws.repositories.splice(idx, 1);
101
- await saveWorkspaceConfig(options.configDir, global.activeWorkspace, ws);
101
+ await saveTenantConfig(options.configDir, global.activeTenant, ws);
102
102
  process.stdout.write(`Removed repository: ${repoSpec}\n`);
103
103
  }
@@ -1,5 +1,5 @@
1
- import { runCli as orchestratorRunCli } from "@hojinzs/gh-symphony-orchestrator";
2
- import { resolveRuntimeRoot, resolveWorkspaceConfig, syncWorkspaceToRuntime, } from "../orchestrator-runtime.js";
1
+ import { runCli as orchestratorRunCli } from "@gh-symphony/orchestrator";
2
+ import { resolveRuntimeRoot, resolveTenantConfig, syncTenantToRuntime, } from "../orchestrator-runtime.js";
3
3
  function parseRunArgs(args) {
4
4
  const parsed = {
5
5
  watch: false,
@@ -9,8 +9,8 @@ function parseRunArgs(args) {
9
9
  if (arg === "--watch" || arg === "-w") {
10
10
  parsed.watch = true;
11
11
  }
12
- else if (arg === "--workspace" || arg === "--workspace-id") {
13
- parsed.workspaceId = args[i + 1];
12
+ else if (arg === "--tenant" || arg === "--tenant-id") {
13
+ parsed.tenantId = args[i + 1];
14
14
  i += 1;
15
15
  }
16
16
  else if (!arg?.startsWith("--")) {
@@ -27,21 +27,21 @@ const handler = async (args, options) => {
27
27
  process.exitCode = 2;
28
28
  return;
29
29
  }
30
- const wsConfig = await resolveWorkspaceConfig(options.configDir, parsed.workspaceId);
31
- if (!wsConfig) {
32
- process.stderr.write("No workspace configured. Run 'gh-symphony init' first.\n");
30
+ const tenantConfig = await resolveTenantConfig(options.configDir, parsed.tenantId);
31
+ if (!tenantConfig) {
32
+ process.stderr.write("No tenant configured. Run 'gh-symphony init' first.\n");
33
33
  process.exitCode = 1;
34
34
  return;
35
35
  }
36
36
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
37
- const workspaceId = wsConfig.workspaceId;
38
- await syncWorkspaceToRuntime(options.configDir, wsConfig);
37
+ const tenantId = tenantConfig.tenantId;
38
+ await syncTenantToRuntime(options.configDir, tenantConfig);
39
39
  // Validate the issue identifier belongs to a configured repo
40
40
  const [repoSpec] = parsed.issue.split("#");
41
41
  if (repoSpec &&
42
- !wsConfig.repositories.some((r) => `${r.owner}/${r.name}` === repoSpec)) {
43
- process.stderr.write(`Repository "${repoSpec}" is not configured in this workspace.\n` +
44
- `Configured repos: ${wsConfig.repositories.map((r) => `${r.owner}/${r.name}`).join(", ")}\n`);
42
+ !tenantConfig.repositories.some((r) => `${r.owner}/${r.name}` === repoSpec)) {
43
+ process.stderr.write(`Repository "${repoSpec}" is not configured in this tenant.\n` +
44
+ `Configured repos: ${tenantConfig.repositories.map((r) => `${r.owner}/${r.name}`).join(", ")}\n`);
45
45
  process.exitCode = 1;
46
46
  return;
47
47
  }
@@ -50,8 +50,8 @@ const handler = async (args, options) => {
50
50
  "run-issue",
51
51
  "--runtime-root",
52
52
  runtimeRoot,
53
- "--workspace-id",
54
- workspaceId,
53
+ "--tenant-id",
54
+ tenantId,
55
55
  "--issue",
56
56
  parsed.issue,
57
57
  ]);
@@ -61,8 +61,8 @@ const handler = async (args, options) => {
61
61
  "status",
62
62
  "--runtime-root",
63
63
  runtimeRoot,
64
- "--workspace-id",
65
- workspaceId,
64
+ "--tenant-id",
65
+ tenantId,
66
66
  ]);
67
67
  }
68
68
  };
@@ -1,24 +1,11 @@
1
- import { writeFile, mkdir } from "node:fs/promises";
2
- import { dirname } from "node:path";
1
+ import { writeFile, mkdir, readFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  import { daemonPidPath, orchestratorLogPath, logsDir } from "../config.js";
5
- import { OrchestratorService, createStore, startOrchestratorStatusServer, } from "@hojinzs/gh-symphony-orchestrator";
6
- import { resolveWorkspaceConfig, resolveRuntimeRoot, syncWorkspaceToRuntime, } from "../orchestrator-runtime.js";
7
- // ── ANSI helpers ──────────────────────────────────────────────────────────────
8
- const ESC = "\x1b[";
9
- const _bold = (s) => `${ESC}1m${s}${ESC}0m`;
10
- const _dim = (s) => `${ESC}2m${s}${ESC}0m`;
11
- const _green = (s) => `${ESC}32m${s}${ESC}0m`;
12
- const _red = (s) => `${ESC}31m${s}${ESC}0m`;
13
- const _yellow = (s) => `${ESC}33m${s}${ESC}0m`;
14
- const _cyan = (s) => `${ESC}36m${s}${ESC}0m`;
15
- let noColor = false;
16
- const bold = (s) => (noColor ? s : _bold(s));
17
- const dim = (s) => (noColor ? s : _dim(s));
18
- const green = (s) => (noColor ? s : _green(s));
19
- const red = (s) => (noColor ? s : _red(s));
20
- const yellow = (s) => (noColor ? s : _yellow(s));
21
- const cyan = (s) => (noColor ? s : _cyan(s));
5
+ import { OrchestratorService, createStore, startOrchestratorStatusServer, } from "@gh-symphony/orchestrator";
6
+ import { resolveTenantConfig, resolveRuntimeRoot, syncTenantToRuntime, } from "../orchestrator-runtime.js";
7
+ import { bold, dim, green, red, yellow, cyan, setNoColor } from "../ansi.js";
8
+ import { getGhToken } from "../github/gh-auth.js";
22
9
  function timestamp() {
23
10
  const now = new Date();
24
11
  const hh = String(now.getHours()).padStart(2, "0");
@@ -37,8 +24,8 @@ function parseStartArgs(args) {
37
24
  if (arg === "--daemon" || arg === "-d") {
38
25
  parsed.daemon = true;
39
26
  }
40
- if (arg === "--workspace" || arg === "--workspace-id") {
41
- parsed.workspaceId = args[i + 1];
27
+ if (arg === "--tenant" || arg === "--tenant-id") {
28
+ parsed.tenantId = args[i + 1];
42
29
  i += 1;
43
30
  }
44
31
  }
@@ -47,14 +34,14 @@ function parseStartArgs(args) {
47
34
  // ── Tick logging ──────────────────────────────────────────────────────────────
48
35
  function logTickResult(snapshots, prevSnapshots, isFirst) {
49
36
  for (const snap of snapshots) {
50
- const prev = prevSnapshots.find((p) => p.workspaceId === snap.workspaceId);
37
+ const prev = prevSnapshots.find((p) => p.tenantId === snap.tenantId);
51
38
  if (isFirst) {
52
39
  const healthColor = snap.health === "degraded"
53
40
  ? red
54
41
  : snap.health === "running"
55
42
  ? green
56
43
  : cyan;
57
- logLine(green("\u25CF"), `Workspace ${bold(snap.slug)} connected ${dim("(")}${healthColor(snap.health)}${dim(")")}`);
44
+ logLine(green("\u25CF"), `Tenant ${bold(snap.slug)} connected ${dim("(")}${healthColor(snap.health)}${dim(")")}`);
58
45
  if (snap.summary.activeRuns > 0) {
59
46
  logLine(cyan("\u25B8"), `${snap.summary.activeRuns} active run(s)`);
60
47
  }
@@ -83,7 +70,7 @@ function logTickResult(snapshots, prevSnapshots, isFirst) {
83
70
  const prevRunIds = new Set(prev?.activeRuns.map((r) => r.runId) ?? []);
84
71
  for (const run of snap.activeRuns) {
85
72
  if (!prevRunIds.has(run.runId)) {
86
- logLine(cyan("\u25B8"), `Run started: ${bold(run.issueIdentifier)} ${dim("phase=")}${run.phase} ${dim("status=")}${run.status}`);
73
+ logLine(cyan("\u25B8"), `Run started: ${bold(run.issueIdentifier)} ${dim("state=")}${run.issueState} ${dim("status=")}${run.status}`);
87
74
  }
88
75
  }
89
76
  // Completed runs (were active, now gone)
@@ -126,37 +113,46 @@ function logTickResult(snapshots, prevSnapshots, isFirst) {
126
113
  }
127
114
  // ── Handler ───────────────────────────────────────────────────────────────────
128
115
  const handler = async (args, options) => {
129
- noColor = options.noColor;
116
+ setNoColor(options.noColor);
130
117
  const parsed = parseStartArgs(args);
131
- const wsConfig = await resolveWorkspaceConfig(options.configDir, parsed.workspaceId);
132
- if (!wsConfig) {
133
- process.stderr.write("No workspace configured. Run 'gh-symphony init' first.\n");
118
+ const tenantConfig = await resolveTenantConfig(options.configDir, parsed.tenantId);
119
+ if (!tenantConfig) {
120
+ process.stderr.write("No tenant configured. Run 'gh-symphony init' first.\n");
134
121
  process.exitCode = 1;
135
122
  return;
136
123
  }
137
124
  const runtimeRoot = resolveRuntimeRoot(options.configDir);
138
- const workspaceId = wsConfig.workspaceId;
139
- await syncWorkspaceToRuntime(options.configDir, wsConfig);
125
+ const tenantId = tenantConfig.tenantId;
126
+ await syncTenantToRuntime(options.configDir, tenantConfig);
140
127
  if (parsed.daemon) {
141
- await startDaemon(options, workspaceId);
128
+ await startDaemon(options, tenantId);
142
129
  return;
143
130
  }
144
131
  // ── 5.1: Foreground mode with live logging ────────────────────────────────
132
+ if (!process.env.GITHUB_GRAPHQL_TOKEN) {
133
+ try {
134
+ process.env.GITHUB_GRAPHQL_TOKEN = getGhToken();
135
+ }
136
+ catch {
137
+ // gh CLI not installed/authenticated — GITHUB_GRAPHQL_TOKEN stays unset
138
+ // Workers will fail if token is needed but not available
139
+ }
140
+ }
145
141
  const store = createStore(runtimeRoot);
146
142
  const service = new OrchestratorService(store);
147
143
  // Start status server
148
144
  startOrchestratorStatusServer({
149
145
  host: "127.0.0.1",
150
146
  port: 4680,
151
- getWorkspaceStatus: {
147
+ getTenantStatus: {
152
148
  all: () => service.status(),
153
- byWorkspaceId: async (id) => {
149
+ byTenantId: async (id) => {
154
150
  const [snapshot] = await service.status(id);
155
151
  return snapshot ?? null;
156
152
  },
157
153
  },
158
154
  });
159
- logLine(green("\u25B2"), `Starting orchestrator for workspace: ${bold(workspaceId)}`);
155
+ logLine(green("\u25B2"), `Starting orchestrator for tenant: ${bold(tenantId)}`);
160
156
  logLine(dim("\u00B7"), dim("Press Ctrl+C to stop"));
161
157
  let running = true;
162
158
  const shutdown = () => {
@@ -170,8 +166,19 @@ const handler = async (args, options) => {
170
166
  let isFirst = true;
171
167
  while (running) {
172
168
  try {
173
- const snapshots = await service.runOnce({ workspaceId });
169
+ const snapshots = await service.runOnce({ tenantId });
174
170
  logTickResult(snapshots, prevSnapshots, isFirst);
171
+ if (!isFirst) {
172
+ for (const snap of snapshots) {
173
+ const prev = prevSnapshots.find((p) => p.tenantId === snap.tenantId);
174
+ const currentRunIds = new Set(snap.activeRuns.map((r) => r.runId));
175
+ for (const prevRun of prev?.activeRuns ?? []) {
176
+ if (!currentRunIds.has(prevRun.runId)) {
177
+ await tailWorkerLog(runtimeRoot, prevRun.runId, prevRun.issueIdentifier);
178
+ }
179
+ }
180
+ }
181
+ }
175
182
  prevSnapshots = snapshots;
176
183
  isFirst = false;
177
184
  }
@@ -182,14 +189,31 @@ const handler = async (args, options) => {
182
189
  await new Promise((r) => setTimeout(r, 30_000));
183
190
  }
184
191
  };
192
+ async function tailWorkerLog(runtimeRoot, runId, issueIdentifier) {
193
+ try {
194
+ const logPath = join(runtimeRoot, "orchestrator", "runs", runId, "worker.log");
195
+ const content = await readFile(logPath, "utf8");
196
+ const lines = content.split("\n").filter((l) => l.trim());
197
+ if (lines.length === 0)
198
+ return;
199
+ const tail = lines.slice(-30);
200
+ logLine(red("\u2717"), red(`Worker stderr (${issueIdentifier}):`));
201
+ for (const line of tail) {
202
+ process.stdout.write(` ${dim(line)}\n`);
203
+ }
204
+ }
205
+ catch {
206
+ // worker.log 없거나 읽기 실패 시 무시
207
+ }
208
+ }
185
209
  export default handler;
186
210
  // ── 5.2: Daemon mode ─────────────────────────────────────────────────────────
187
- async function startDaemon(options, workspaceId) {
211
+ async function startDaemon(options, tenantId) {
188
212
  const logPath = orchestratorLogPath(options.configDir);
189
213
  await mkdir(logsDir(options.configDir), { recursive: true });
190
214
  const { openSync } = await import("node:fs");
191
215
  const logFd = openSync(logPath, "a");
192
- const child = spawn(process.execPath, [process.argv[1], "start", "--workspace", workspaceId], {
216
+ const child = spawn(process.execPath, [process.argv[1], "start", "--tenant", tenantId], {
193
217
  cwd: process.cwd(),
194
218
  env: {
195
219
  ...process.env,