@gh-symphony/cli 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,17 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ GhAuthError,
3
4
  GitHubScopeError,
4
5
  checkRequiredScopes,
5
6
  createClient,
7
+ getGhTokenWithSource,
6
8
  getProjectDetail,
7
9
  listUserProjects,
10
+ resolveGitHubAuth,
8
11
  validateToken
9
- } from "./chunk-62L6QQE6.js";
10
- import {
11
- GhAuthError,
12
- ensureGhAuth,
13
- getGhToken
14
- } from "./chunk-7UBUBSMH.js";
12
+ } from "./chunk-TILHWBP6.js";
15
13
  import {
16
14
  loadGlobalConfig,
17
15
  saveGlobalConfig,
@@ -21,7 +19,7 @@ import {
21
19
  // src/commands/init.ts
22
20
  import * as p from "@clack/prompts";
23
21
  import { createHash } from "crypto";
24
- import { mkdir as mkdir3, rename, writeFile as writeFile3 } from "fs/promises";
22
+ import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
25
23
  import { basename, dirname as dirname2, join as join3, relative, resolve } from "path";
26
24
 
27
25
  // src/mapping/smart-defaults.ts
@@ -468,16 +466,6 @@ function generateContextYamlString(context) {
468
466
  lines.push(` agent_command: ${yamlQuote(context.runtime.agent_command)}`);
469
467
  return lines.join("\n") + "\n";
470
468
  }
471
- async function writeContextYaml(outputDir, context) {
472
- await mkdir(outputDir, { recursive: true });
473
- const contextPath = `${outputDir}/.gh-symphony/context.yaml`;
474
- await mkdir(dirname(contextPath), { recursive: true });
475
- const temporaryPath = `${contextPath}.tmp`;
476
- const yamlContent = generateContextYamlString(context);
477
- await writeFile(temporaryPath, yamlContent, "utf8");
478
- const { rename: rename2 } = await import("fs/promises");
479
- await rename2(temporaryPath, contextPath);
480
- }
481
469
  function buildContextYaml(params) {
482
470
  const columns = params.statusField.options.map((option) => {
483
471
  const roleMapping = inferStateRole(option.name);
@@ -858,7 +846,7 @@ function resolveRoleAction(role) {
858
846
  }
859
847
 
860
848
  // src/skills/skill-writer.ts
861
- import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile2 } from "fs/promises";
849
+ import { mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
862
850
  import { join as join2 } from "path";
863
851
  function normalizeRuntimeForSkills(runtime) {
864
852
  if (runtime === "claude-code" || runtime.includes("claude-code")) {
@@ -879,44 +867,18 @@ function resolveSkillsDir(repoRoot, runtime) {
879
867
  }
880
868
  return null;
881
869
  }
882
- async function writeSkillFile(skillsDir, template, context, options) {
883
- const skillDir = join2(skillsDir, template.name);
884
- const filePath = join2(skillDir, template.fileName);
885
- if (!options?.overwrite) {
886
- try {
887
- await readFile2(filePath, "utf8");
888
- return { written: false, path: filePath };
889
- } catch (error) {
890
- const err = error;
891
- if (err.code !== "ENOENT") {
892
- throw error;
893
- }
894
- }
895
- }
896
- await mkdir2(skillDir, { recursive: true });
897
- const content = template.generate(context);
898
- const temporaryPath = `${filePath}.tmp`;
899
- await writeFile2(temporaryPath, content, "utf8");
900
- const { rename: rename2 } = await import("fs/promises");
901
- await rename2(temporaryPath, filePath);
902
- return { written: true, path: filePath };
903
- }
904
- async function writeAllSkills(repoRoot, runtime, templates, context, options) {
870
+ function buildSkillFilePlans(repoRoot, runtime, templates, context) {
905
871
  const skillsDir = resolveSkillsDir(repoRoot, runtime);
906
872
  if (!skillsDir) {
907
- return { written: [], skipped: [] };
908
- }
909
- const written = [];
910
- const skipped = [];
911
- for (const template of templates) {
912
- const result = await writeSkillFile(skillsDir, template, context, options);
913
- if (result.written) {
914
- written.push(result.path);
915
- } else {
916
- skipped.push(result.path);
917
- }
873
+ return { skillsDir: null, files: [] };
918
874
  }
919
- return { written, skipped };
875
+ return {
876
+ skillsDir,
877
+ files: templates.map((template) => ({
878
+ path: join2(skillsDir, template.name, template.fileName),
879
+ content: template.generate(context)
880
+ }))
881
+ };
920
882
  }
921
883
 
922
884
  // src/skills/templates/document.ts
@@ -1454,6 +1416,7 @@ async function abortIfCancelled(input) {
1454
1416
  }
1455
1417
  function parseInitFlags(args) {
1456
1418
  const flags = {
1419
+ dryRun: false,
1457
1420
  nonInteractive: false,
1458
1421
  skipSkills: false,
1459
1422
  skipContext: false
@@ -1462,6 +1425,9 @@ function parseInitFlags(args) {
1462
1425
  const arg = args[i];
1463
1426
  const next = args[i + 1];
1464
1427
  switch (arg) {
1428
+ case "--dry-run":
1429
+ flags.dryRun = true;
1430
+ break;
1465
1431
  case "--non-interactive":
1466
1432
  flags.nonInteractive = true;
1467
1433
  break;
@@ -1489,29 +1455,145 @@ var handler = async (args, options) => {
1489
1455
  await runNonInteractive(flags, options);
1490
1456
  return;
1491
1457
  }
1492
- await runInteractive(options);
1458
+ await runInteractive(flags, options);
1493
1459
  };
1494
1460
  var init_default = handler;
1495
- async function writeEcosystem(opts) {
1461
+ async function resolveChangeStatus(path, content, mode) {
1462
+ try {
1463
+ const existing = await readFile3(path, "utf8");
1464
+ if (mode === "create-only") {
1465
+ return "unchanged";
1466
+ }
1467
+ return existing === content ? "unchanged" : "update";
1468
+ } catch (error) {
1469
+ const err = error;
1470
+ if (err.code === "ENOENT") {
1471
+ return "create";
1472
+ }
1473
+ throw error;
1474
+ }
1475
+ }
1476
+ async function planFileChange(input) {
1477
+ return {
1478
+ ...input,
1479
+ status: await resolveChangeStatus(input.path, input.content, input.mode)
1480
+ };
1481
+ }
1482
+ async function writePlannedFile(file) {
1483
+ if (file.status === "unchanged") {
1484
+ return false;
1485
+ }
1486
+ await mkdir3(dirname2(file.path), { recursive: true });
1487
+ const temporaryPath = `${file.path}.tmp`;
1488
+ await writeFile3(temporaryPath, file.content, "utf8");
1489
+ await rename2(temporaryPath, file.path);
1490
+ return true;
1491
+ }
1492
+ function resolveStatusField(projectDetail) {
1493
+ return projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ?? projectDetail.statusFields[0] ?? null;
1494
+ }
1495
+ function buildAutomaticStateMappings(statusField) {
1496
+ const mappings = {};
1497
+ for (const mapping of inferAllStateRoles(statusField.options.map((o) => o.name))) {
1498
+ if (mapping.role) {
1499
+ mappings[mapping.columnName] = { role: mapping.role };
1500
+ }
1501
+ }
1502
+ return mappings;
1503
+ }
1504
+ async function promptStateMappings(statusField, options) {
1505
+ const mappings = {};
1506
+ const inferred = inferAllStateRoles(statusField.options.map((o) => o.name));
1507
+ p.log.info(
1508
+ `Found ${statusField.options.length} status columns on field "${statusField.name}".`
1509
+ );
1510
+ for (const mapping of inferred) {
1511
+ const roleOptions = [
1512
+ { value: "active", label: "Active (agent works on this)" },
1513
+ { value: "wait", label: "Wait (human review / hold)" },
1514
+ { value: "terminal", label: "Terminal (completed)" }
1515
+ ];
1516
+ const defaultRole = mapping.role ?? "wait";
1517
+ const sortedOptions = [
1518
+ roleOptions.find((o) => o.value === defaultRole),
1519
+ ...roleOptions.filter((o) => o.value !== defaultRole)
1520
+ ];
1521
+ const selectedRole = await abortIfCancelled(
1522
+ p.select({
1523
+ message: `${options?.stepLabel ?? "Step 2/2"} \u2014 Map column "${mapping.columnName}":${mapping.confidence === "high" ? " (auto-detected)" : ""}`,
1524
+ options: sortedOptions
1525
+ })
1526
+ );
1527
+ mappings[mapping.columnName] = { role: selectedRole };
1528
+ }
1529
+ return mappings;
1530
+ }
1531
+ async function planWorkflowArtifacts(opts) {
1532
+ const workflowMd = generateWorkflowMarkdown({
1533
+ projectId: opts.projectDetail.id,
1534
+ stateFieldName: opts.statusField.name,
1535
+ mappings: opts.mappings,
1536
+ lifecycle: toWorkflowLifecycleConfig(opts.statusField.name, opts.mappings),
1537
+ runtime: opts.runtime
1538
+ });
1539
+ const workflowPlan = await planFileChange({
1540
+ path: opts.outputPath,
1541
+ label: "WORKFLOW.md",
1542
+ content: workflowMd,
1543
+ mode: "overwrite"
1544
+ });
1545
+ const ecosystemPlan = await planEcosystem({
1546
+ cwd: opts.cwd,
1547
+ projectDetail: opts.projectDetail,
1548
+ statusField: opts.statusField,
1549
+ runtime: opts.runtime,
1550
+ skipSkills: opts.skipSkills,
1551
+ skipContext: opts.skipContext
1552
+ });
1553
+ return {
1554
+ outputPath: opts.outputPath,
1555
+ workflowMd,
1556
+ workflowPlan,
1557
+ ecosystemPlan
1558
+ };
1559
+ }
1560
+ async function writeWorkflowPlan(workflowPlan) {
1561
+ return writePlannedFile(workflowPlan);
1562
+ }
1563
+ function summarizeEnvironment(env) {
1564
+ return [
1565
+ `Package manager ${env.packageManager ?? "none"}${env.lockfile ? ` (${env.lockfile})` : ""}`,
1566
+ `Scripts test=${env.testCommand ?? "none"} | lint=${env.lintCommand ?? "none"} | build=${env.buildCommand ?? "none"}`,
1567
+ `CI ${env.ciPlatform ?? "none"}`,
1568
+ `Monorepo ${env.monorepo ? "yes" : "no"}`,
1569
+ `Existing skills ${env.existingSkills.length === 0 ? "none" : env.existingSkills.join(", ")}`
1570
+ ];
1571
+ }
1572
+ async function planEcosystem(opts) {
1496
1573
  const { cwd, projectDetail, statusField, runtime, skipSkills, skipContext } = opts;
1497
1574
  const ghSymphonyDir = join3(cwd, ".gh-symphony");
1498
- await mkdir3(ghSymphonyDir, { recursive: true });
1499
- const env = await detectEnvironment(cwd);
1500
- let contextYamlWritten = false;
1575
+ const environment = await detectEnvironment(cwd);
1576
+ const files = [];
1501
1577
  if (!skipContext) {
1502
1578
  const contextYaml = buildContextYaml({
1503
1579
  projectDetail,
1504
1580
  statusField,
1505
- detectedEnvironment: env,
1581
+ detectedEnvironment: environment,
1506
1582
  runtime: {
1507
1583
  agent: runtime,
1508
1584
  agent_command: runtime === "codex" ? "bash -lc codex app-server" : runtime === "claude-code" ? "bash -lc claude-code" : runtime
1509
1585
  }
1510
1586
  });
1511
- await writeContextYaml(cwd, contextYaml);
1512
- contextYamlWritten = true;
1587
+ files.push(
1588
+ await planFileChange({
1589
+ path: join3(ghSymphonyDir, "context.yaml"),
1590
+ label: "Context metadata",
1591
+ content: generateContextYamlString(contextYaml),
1592
+ mode: "overwrite"
1593
+ })
1594
+ );
1513
1595
  }
1514
- const refWorkflow = generateReferenceWorkflow({
1596
+ const referenceWorkflow = generateReferenceWorkflow({
1515
1597
  runtime,
1516
1598
  statusColumns: statusField.options.map((o) => ({
1517
1599
  name: o.name,
@@ -1519,43 +1601,99 @@ async function writeEcosystem(opts) {
1519
1601
  })),
1520
1602
  projectId: projectDetail.id
1521
1603
  });
1522
- const refPath = join3(ghSymphonyDir, "reference-workflow.md");
1523
- const tmpRef = refPath + ".tmp";
1524
- await writeFile3(tmpRef, refWorkflow, "utf8");
1525
- await rename(tmpRef, refPath);
1526
- const skillsDir = resolveSkillsDir(cwd, runtime);
1527
- let skillsWritten = [];
1528
- let skillsSkipped = [];
1604
+ files.push(
1605
+ await planFileChange({
1606
+ path: join3(ghSymphonyDir, "reference-workflow.md"),
1607
+ label: "Reference workflow",
1608
+ content: referenceWorkflow,
1609
+ mode: "overwrite"
1610
+ })
1611
+ );
1612
+ const skillsDir = skipSkills ? null : resolveSkillsDir(cwd, runtime);
1529
1613
  if (!skipSkills && skillsDir) {
1530
- const result = await writeAllSkills(cwd, runtime, ALL_SKILL_TEMPLATES, {
1614
+ const { files: plannedSkills } = buildSkillFilePlans(
1615
+ cwd,
1531
1616
  runtime,
1532
- projectId: projectDetail.id,
1533
- githubProjectTitle: projectDetail.title,
1534
- repositories: projectDetail.linkedRepositories.map((r) => ({
1535
- owner: r.owner,
1536
- name: r.name
1537
- })),
1538
- statusColumns: statusField.options.map((o) => ({
1539
- id: o.id,
1540
- name: o.name,
1541
- role: null
1542
- })),
1543
- statusFieldId: statusField.id,
1544
- contextYamlPath: ".gh-symphony/context.yaml",
1545
- referenceWorkflowPath: ".gh-symphony/reference-workflow.md"
1546
- });
1547
- skillsWritten = result.written.map((p2) => basename(dirname2(p2)));
1548
- skillsSkipped = result.skipped.map((p2) => basename(dirname2(p2)));
1617
+ ALL_SKILL_TEMPLATES,
1618
+ {
1619
+ runtime,
1620
+ projectId: projectDetail.id,
1621
+ githubProjectTitle: projectDetail.title,
1622
+ repositories: projectDetail.linkedRepositories.map((r) => ({
1623
+ owner: r.owner,
1624
+ name: r.name
1625
+ })),
1626
+ statusColumns: statusField.options.map((o) => ({
1627
+ id: o.id,
1628
+ name: o.name,
1629
+ role: null
1630
+ })),
1631
+ statusFieldId: statusField.id,
1632
+ contextYamlPath: ".gh-symphony/context.yaml",
1633
+ referenceWorkflowPath: ".gh-symphony/reference-workflow.md"
1634
+ }
1635
+ );
1636
+ for (const plannedSkill of plannedSkills) {
1637
+ files.push(
1638
+ await planFileChange({
1639
+ path: plannedSkill.path,
1640
+ label: `Skill ${basename(dirname2(plannedSkill.path))}`,
1641
+ content: plannedSkill.content,
1642
+ mode: "create-only"
1643
+ })
1644
+ );
1645
+ }
1549
1646
  }
1550
1647
  return {
1551
1648
  projectId: projectDetail.id,
1552
1649
  githubProjectTitle: projectDetail.title,
1553
1650
  runtime,
1554
1651
  skillsDir,
1652
+ environment,
1653
+ files
1654
+ };
1655
+ }
1656
+ async function writeEcosystem(opts) {
1657
+ const plan = await planEcosystem(opts);
1658
+ await mkdir3(join3(opts.cwd, ".gh-symphony"), { recursive: true });
1659
+ const contextYamlPath = join3(opts.cwd, ".gh-symphony", "context.yaml");
1660
+ const referenceWorkflowPath = join3(
1661
+ opts.cwd,
1662
+ ".gh-symphony",
1663
+ "reference-workflow.md"
1664
+ );
1665
+ let contextYamlWritten = false;
1666
+ let referenceWorkflowWritten = false;
1667
+ const skillsWritten = [];
1668
+ const skillsSkipped = [];
1669
+ for (const file of plan.files) {
1670
+ const written = await writePlannedFile(file);
1671
+ if (file.path === contextYamlPath) {
1672
+ contextYamlWritten = written;
1673
+ continue;
1674
+ }
1675
+ if (file.path === referenceWorkflowPath) {
1676
+ referenceWorkflowWritten = written;
1677
+ continue;
1678
+ }
1679
+ if (file.label.startsWith("Skill ")) {
1680
+ const skillName = basename(dirname2(file.path));
1681
+ if (written) {
1682
+ skillsWritten.push(skillName);
1683
+ } else {
1684
+ skillsSkipped.push(skillName);
1685
+ }
1686
+ }
1687
+ }
1688
+ return {
1689
+ projectId: plan.projectId,
1690
+ githubProjectTitle: plan.githubProjectTitle,
1691
+ runtime: plan.runtime,
1692
+ skillsDir: plan.skillsDir,
1555
1693
  contextYamlWritten,
1556
- referenceWorkflowWritten: true,
1557
- skillsWritten,
1558
- skillsSkipped
1694
+ referenceWorkflowWritten,
1695
+ skillsWritten: skillsWritten.sort(),
1696
+ skillsSkipped: skillsSkipped.sort()
1559
1697
  };
1560
1698
  }
1561
1699
  function printEcosystemSummary(result, workflowPath, opts) {
@@ -1598,10 +1736,65 @@ function printEcosystemSummary(result, workflowPath, opts) {
1598
1736
  process.stdout.write(lines.map((l) => ` ${l}`).join("\n") + "\n");
1599
1737
  }
1600
1738
  }
1739
+ function renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
1740
+ const cwd = process.cwd();
1741
+ const relWorkflow = relative(cwd, workflowPath) || "WORKFLOW.md";
1742
+ const statusIcon = {
1743
+ create: "+",
1744
+ update: "~",
1745
+ unchanged: "="
1746
+ };
1747
+ const lines = [];
1748
+ lines.push("Init dry-run preview");
1749
+ lines.push(
1750
+ `GitHub Project ${ecosystemPlan.githubProjectTitle} (${ecosystemPlan.projectId})`
1751
+ );
1752
+ lines.push(`Runtime ${ecosystemPlan.runtime}`);
1753
+ lines.push("");
1754
+ lines.push("Planned file changes");
1755
+ lines.push(
1756
+ ` ${statusIcon[workflowPlan.status]} ${workflowPlan.status.padEnd(9)} WORKFLOW.md ${relWorkflow}`
1757
+ );
1758
+ for (const file of ecosystemPlan.files) {
1759
+ const relPath = relative(cwd, file.path) || file.path;
1760
+ lines.push(
1761
+ ` ${statusIcon[file.status]} ${file.status.padEnd(9)} ${file.label.padEnd(36)} ${relPath}`
1762
+ );
1763
+ }
1764
+ lines.push("");
1765
+ lines.push("Detected environment inputs");
1766
+ for (const line of summarizeEnvironment(ecosystemPlan.environment)) {
1767
+ lines.push(` ${line}`);
1768
+ }
1769
+ lines.push("");
1770
+ lines.push("Dry run only. No files were written.");
1771
+ return lines.join("\n") + "\n";
1772
+ }
1773
+ function buildDryRunJsonResult(workflowPath, workflowPlan, ecosystemPlan) {
1774
+ return {
1775
+ dryRun: true,
1776
+ output: workflowPath,
1777
+ projectId: ecosystemPlan.projectId,
1778
+ githubProjectTitle: ecosystemPlan.githubProjectTitle,
1779
+ runtime: ecosystemPlan.runtime,
1780
+ files: [workflowPlan, ...ecosystemPlan.files].map((file) => ({
1781
+ path: file.path,
1782
+ label: file.label,
1783
+ status: file.status,
1784
+ mode: file.mode
1785
+ })),
1786
+ environment: ecosystemPlan.environment
1787
+ };
1788
+ }
1789
+ function printDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
1790
+ process.stdout.write(
1791
+ renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan)
1792
+ );
1793
+ }
1601
1794
  async function runNonInteractive(flags, options) {
1602
1795
  let token;
1603
1796
  try {
1604
- token = getGhToken();
1797
+ token = getGhTokenWithSource().token;
1605
1798
  } catch {
1606
1799
  process.stderr.write(
1607
1800
  "Error: GitHub token not found. Run 'gh auth login --scopes repo,read:org,project' or set GITHUB_GRAPHQL_TOKEN.\n"
@@ -1649,20 +1842,13 @@ async function runNonInteractive(flags, options) {
1649
1842
  process.exitCode = 1;
1650
1843
  return;
1651
1844
  }
1652
- const statusField = githubProject.statusFields.find((f) => f.name.toLowerCase() === "status") ?? githubProject.statusFields[0];
1845
+ const statusField = resolveStatusField(githubProject);
1653
1846
  if (!statusField) {
1654
1847
  process.stderr.write("Error: No status field found on the project.\n");
1655
1848
  process.exitCode = 1;
1656
1849
  return;
1657
1850
  }
1658
- const columnNames = statusField.options.map((o) => o.name);
1659
- const inferred = inferAllStateRoles(columnNames);
1660
- const mappings = {};
1661
- for (const mapping of inferred) {
1662
- if (mapping.role) {
1663
- mappings[mapping.columnName] = { role: mapping.role };
1664
- }
1665
- }
1851
+ const mappings = buildAutomaticStateMappings(statusField);
1666
1852
  const validation = validateStateMapping(mappings);
1667
1853
  if (!validation.valid) {
1668
1854
  process.stderr.write(
@@ -1673,16 +1859,30 @@ Run without --non-interactive for manual mapping.
1673
1859
  process.exitCode = 1;
1674
1860
  return;
1675
1861
  }
1676
- const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
1677
1862
  const outputPath = resolve(flags.output ?? "WORKFLOW.md");
1678
- const workflowMd = generateWorkflowMarkdown({
1679
- projectId: githubProject.id,
1680
- stateFieldName: statusField.name,
1863
+ const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
1864
+ cwd: process.cwd(),
1865
+ outputPath,
1866
+ projectDetail: githubProject,
1867
+ statusField,
1681
1868
  mappings,
1682
- lifecycle: lifecycleConfig,
1683
- runtime: "codex"
1869
+ runtime: "codex",
1870
+ skipSkills: flags.skipSkills,
1871
+ skipContext: flags.skipContext
1684
1872
  });
1685
- await writeFile3(outputPath, workflowMd, "utf8");
1873
+ if (flags.dryRun) {
1874
+ if (options.json) {
1875
+ process.stdout.write(
1876
+ JSON.stringify(
1877
+ buildDryRunJsonResult(outputPath, workflowPlan, ecosystemPlan)
1878
+ ) + "\n"
1879
+ );
1880
+ return;
1881
+ }
1882
+ printDryRunPreview(outputPath, workflowPlan, ecosystemPlan);
1883
+ return;
1884
+ }
1885
+ await writeWorkflowPlan(workflowPlan);
1686
1886
  const ecosystemResult = await writeEcosystem({
1687
1887
  cwd: process.cwd(),
1688
1888
  projectDetail: githubProject,
@@ -1693,7 +1893,7 @@ Run without --non-interactive for manual mapping.
1693
1893
  });
1694
1894
  if (options.json) {
1695
1895
  process.stdout.write(
1696
- JSON.stringify({ output: outputPath, status: "created" }) + "\n"
1896
+ JSON.stringify({ output: outputPath, status: workflowPlan.status }) + "\n"
1697
1897
  );
1698
1898
  } else {
1699
1899
  printEcosystemSummary(ecosystemResult, outputPath, {
@@ -1702,36 +1902,23 @@ Run without --non-interactive for manual mapping.
1702
1902
  });
1703
1903
  }
1704
1904
  }
1705
- async function runInteractive(options) {
1905
+ async function runInteractive(flags, options) {
1706
1906
  p.intro("gh-symphony \u2014 WORKFLOW.md Setup");
1707
- await runInteractiveStandalone(options);
1907
+ await runInteractiveStandalone(flags, options);
1708
1908
  }
1709
- async function runInteractiveStandalone(_options) {
1909
+ async function runInteractiveStandalone(flags, _options) {
1710
1910
  const s1 = p.spinner();
1711
- s1.start("Checking gh CLI authentication...");
1911
+ s1.start("Checking GitHub authentication...");
1712
1912
  let client;
1713
1913
  try {
1714
- const { token } = ensureGhAuth();
1715
- client = createClient(token);
1716
- s1.stop("Authenticated via gh CLI");
1914
+ const auth = await resolveGitHubAuth();
1915
+ const sourceLabel = auth.source === "env" ? "GITHUB_GRAPHQL_TOKEN" : "gh CLI";
1916
+ client = createClient(auth.token);
1917
+ s1.stop(`Authenticated via ${sourceLabel} as ${auth.login}`);
1717
1918
  } catch (error) {
1718
1919
  s1.stop("Authentication failed.");
1719
1920
  if (error instanceof GhAuthError) {
1720
- if (error.code === "not_installed") {
1721
- p.log.error(
1722
- "gh CLI\uAC00 \uC124\uCE58\uB418\uC5B4 \uC788\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. https://cli.github.com \uC5D0\uC11C \uC124\uCE58\uD558\uC138\uC694."
1723
- );
1724
- } else if (error.code === "not_authenticated") {
1725
- p.log.error(
1726
- "gh auth login --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
1727
- );
1728
- } else if (error.code === "missing_scopes") {
1729
- p.log.error(
1730
- "gh auth refresh --scopes repo,read:org,project \uB97C \uC2E4\uD589\uD558\uC138\uC694."
1731
- );
1732
- } else {
1733
- p.log.error(error.message);
1734
- }
1921
+ p.log.error(error.message);
1735
1922
  } else {
1736
1923
  p.log.error(error instanceof Error ? error.message : "Unknown error");
1737
1924
  }
@@ -1749,7 +1936,7 @@ async function runInteractiveStandalone(_options) {
1749
1936
  } catch (error) {
1750
1937
  s2.stop("Failed to load projects.");
1751
1938
  if (error instanceof GitHubScopeError) {
1752
- displayScopeError(error, "gh-symphony init");
1939
+ displayScopeError(error, "gh-symphony workflow init");
1753
1940
  } else {
1754
1941
  p.log.error(error instanceof Error ? error.message : "Unknown error");
1755
1942
  }
@@ -1786,7 +1973,7 @@ async function runInteractiveStandalone(_options) {
1786
1973
  process.exitCode = 1;
1787
1974
  return;
1788
1975
  }
1789
- const statusField = projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ?? projectDetail.statusFields[0];
1976
+ const statusField = resolveStatusField(projectDetail);
1790
1977
  if (!statusField) {
1791
1978
  p.log.error(
1792
1979
  "No status field found on the project. The project needs a single-select 'Status' field."
@@ -1794,33 +1981,7 @@ async function runInteractiveStandalone(_options) {
1794
1981
  process.exitCode = 1;
1795
1982
  return;
1796
1983
  }
1797
- const columnNames = statusField.options.map((o) => o.name);
1798
- const inferred = inferAllStateRoles(columnNames);
1799
- p.log.info(
1800
- `Found ${columnNames.length} status columns on field "${statusField.name}".`
1801
- );
1802
- const mappings = {};
1803
- for (const mapping of inferred) {
1804
- const roleOptions = [
1805
- { value: "active", label: "Active (agent works on this)" },
1806
- { value: "wait", label: "Wait (human review / hold)" },
1807
- { value: "terminal", label: "Terminal (completed)" }
1808
- ];
1809
- const defaultRole = mapping.role ?? "wait";
1810
- const sortedOptions = [
1811
- roleOptions.find((o) => o.value === defaultRole),
1812
- ...roleOptions.filter((o) => o.value !== defaultRole)
1813
- ];
1814
- const selectedRole = await abortIfCancelled(
1815
- p.select({
1816
- message: `Step 2/2 \u2014 Map column "${mapping.columnName}":${mapping.confidence === "high" ? " (auto-detected)" : ""}`,
1817
- options: sortedOptions
1818
- })
1819
- );
1820
- if (selectedRole !== "skip") {
1821
- mappings[mapping.columnName] = { role: selectedRole };
1822
- }
1823
- }
1984
+ const mappings = await promptStateMappings(statusField);
1824
1985
  const validation = validateStateMapping(mappings);
1825
1986
  if (!validation.valid) {
1826
1987
  p.log.error("Mapping validation failed:");
@@ -1833,23 +1994,29 @@ async function runInteractiveStandalone(_options) {
1833
1994
  for (const warn of validation.warnings) {
1834
1995
  p.log.warn(` \u26A0 ${warn}`);
1835
1996
  }
1836
- const lifecycleConfig = toWorkflowLifecycleConfig(statusField.name, mappings);
1837
- const workflowMd = generateWorkflowMarkdown({
1838
- projectId: projectDetail.id,
1839
- stateFieldName: statusField.name,
1997
+ const outputPath = resolve(flags.output ?? "WORKFLOW.md");
1998
+ const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
1999
+ cwd: process.cwd(),
2000
+ outputPath,
2001
+ projectDetail,
2002
+ statusField,
1840
2003
  mappings,
1841
- lifecycle: lifecycleConfig,
1842
- runtime: "codex"
2004
+ runtime: "codex",
2005
+ skipSkills: flags.skipSkills,
2006
+ skipContext: flags.skipContext
1843
2007
  });
1844
- const outputPath = resolve("WORKFLOW.md");
1845
- await writeFile3(outputPath, workflowMd, "utf8");
2008
+ if (flags.dryRun) {
2009
+ printDryRunPreview(outputPath, workflowPlan, ecosystemPlan);
2010
+ return;
2011
+ }
2012
+ await writeWorkflowPlan(workflowPlan);
1846
2013
  const ecosystemResult = await writeEcosystem({
1847
2014
  cwd: process.cwd(),
1848
2015
  projectDetail,
1849
2016
  statusField,
1850
2017
  runtime: "codex",
1851
- skipSkills: false,
1852
- skipContext: false
2018
+ skipSkills: flags.skipSkills,
2019
+ skipContext: flags.skipContext
1853
2020
  });
1854
2021
  printEcosystemSummary(ecosystemResult, outputPath, {
1855
2022
  interactive: true,
@@ -1893,9 +2060,18 @@ function generateProjectId(githubProjectTitle, uniqueKey) {
1893
2060
  }
1894
2061
 
1895
2062
  export {
2063
+ validateStateMapping,
1896
2064
  abortIfCancelled,
1897
2065
  init_default,
2066
+ resolveStatusField,
2067
+ buildAutomaticStateMappings,
2068
+ promptStateMappings,
2069
+ planWorkflowArtifacts,
2070
+ writeWorkflowPlan,
2071
+ planEcosystem,
1898
2072
  writeEcosystem,
2073
+ renderDryRunPreview,
2074
+ buildDryRunJsonResult,
1899
2075
  writeConfig,
1900
2076
  generateProjectId
1901
2077
  };