@gh-symphony/cli 0.2.5 → 0.4.0

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.
@@ -16,7 +16,7 @@ import {
16
16
  formatClaudePreflightText,
17
17
  resolveClaudeCommandBinary,
18
18
  runClaudePreflight
19
- } from "./chunk-3SKN5L3I.js";
19
+ } from "./chunk-6OPRRC2J.js";
20
20
 
21
21
  // src/mapping/smart-defaults.ts
22
22
  var ROLE_PATTERNS = [
@@ -45,10 +45,9 @@ function inferStateRole(columnName) {
45
45
  function inferAllStateRoles(columnNames) {
46
46
  return columnNames.map(inferStateRole);
47
47
  }
48
- function toWorkflowLifecycleConfig(stateFieldName, mappings) {
48
+ function toWorkflowLifecycleConfig(stateFieldName, mappings, options = {}) {
49
49
  const activeStates = [];
50
50
  const terminalStates = [];
51
- const blockerCheckStates = [];
52
51
  for (const [columnName, mapping] of Object.entries(mappings)) {
53
52
  switch (mapping.role) {
54
53
  case "active":
@@ -61,14 +60,13 @@ function toWorkflowLifecycleConfig(stateFieldName, mappings) {
61
60
  break;
62
61
  }
63
62
  }
64
- if (activeStates.length > 0) {
65
- blockerCheckStates.push(activeStates[0]);
66
- }
63
+ const blockerCheckStates = options.blockerCheckStates ?? [];
67
64
  return {
68
65
  stateFieldName,
69
66
  activeStates,
70
67
  terminalStates,
71
- blockerCheckStates
68
+ blockerCheckStates,
69
+ planningStates: options.planningStates ?? blockerCheckStates
72
70
  };
73
71
  }
74
72
  function validateStateMapping(mappings) {
@@ -100,7 +98,7 @@ import * as p from "@clack/prompts";
100
98
  import { spawnSync } from "child_process";
101
99
  import { createHash } from "crypto";
102
100
  import { chmod, mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
103
- import { basename, dirname as dirname2, join as join3, relative, resolve } from "path";
101
+ import { dirname as dirname3, join as join3, relative, resolve } from "path";
104
102
 
105
103
  // src/prompts/runtime-claude-constraints.ts
106
104
  var CLAUDE_RUNTIME_CONSTRAINTS_SECTION = `## Runtime Constraints
@@ -361,12 +359,18 @@ function buildFrontMatter(input) {
361
359
  lines.push(` - ${state}`);
362
360
  }
363
361
  }
364
- if (input.lifecycle.blockerCheckStates.length > 0) {
365
- lines.push(" blocker_check_states:");
366
- for (const state of input.lifecycle.blockerCheckStates) {
367
- lines.push(` - ${state}`);
368
- }
369
- }
362
+ lines.push(
363
+ ...buildStringListFrontMatter(
364
+ "blocker_check_states",
365
+ input.lifecycle.blockerCheckStates
366
+ )
367
+ );
368
+ lines.push(
369
+ ...buildStringListFrontMatter(
370
+ "planning_states",
371
+ input.lifecycle.planningStates
372
+ )
373
+ );
370
374
  lines.push("polling:");
371
375
  lines.push(` interval_ms: ${input.pollIntervalMs ?? 3e4}`);
372
376
  lines.push("workspace:");
@@ -380,6 +384,12 @@ function buildFrontMatter(input) {
380
384
  lines.push(...buildRuntimeFrontMatter(input.runtime));
381
385
  return lines.join("\n") + "\n";
382
386
  }
387
+ function buildStringListFrontMatter(key, values) {
388
+ if (values.length === 0) {
389
+ return [` ${key}: []`];
390
+ }
391
+ return [` ${key}:`, ...values.map((value) => ` - ${value}`)];
392
+ }
383
393
  function buildPriorityFrontMatter(input) {
384
394
  const lines = [];
385
395
  if (!input.priority) {
@@ -984,7 +994,8 @@ function generateReferenceWorkflow(input) {
984
994
  const terminalColumns = input.statusColumns.filter(
985
995
  (c) => c.role === "terminal"
986
996
  );
987
- const firstActive = activeColumns[0];
997
+ const blockerCheckStates = input.lifecycle?.blockerCheckStates ?? [];
998
+ const planningStates = input.lifecycle?.planningStates ?? blockerCheckStates;
988
999
  if (activeColumns.length > 0) {
989
1000
  lines.push(" active_states:");
990
1001
  for (const col of activeColumns) {
@@ -1001,12 +1012,10 @@ function generateReferenceWorkflow(input) {
1001
1012
  } else {
1002
1013
  lines.push(" terminal_states: [{terminal column names}]");
1003
1014
  }
1004
- if (firstActive) {
1005
- lines.push(" blocker_check_states:");
1006
- lines.push(` - ${firstActive.name}`);
1007
- } else {
1008
- lines.push(" blocker_check_states: [{first active state}]");
1009
- }
1015
+ lines.push(
1016
+ ...buildReferenceStringList("blocker_check_states", blockerCheckStates)
1017
+ );
1018
+ lines.push(...buildReferenceStringList("planning_states", planningStates));
1010
1019
  lines.push("");
1011
1020
  lines.push("# Linear tracker example:");
1012
1021
  lines.push("# tracker:");
@@ -1021,7 +1030,9 @@ function generateReferenceWorkflow(input) {
1021
1030
  lines.push("# - Done");
1022
1031
  lines.push("# - Canceled");
1023
1032
  lines.push("# - Duplicate");
1024
- lines.push("# Linear uses repository-local polling; gh-symphony does not provide");
1033
+ lines.push(
1034
+ "# Linear uses repository-local polling; gh-symphony does not provide"
1035
+ );
1025
1036
  lines.push("# a Linear webhook setup command.");
1026
1037
  lines.push("");
1027
1038
  lines.push("polling:");
@@ -1282,6 +1293,12 @@ function generateReferenceWorkflow(input) {
1282
1293
  lines.push("");
1283
1294
  return lines.join("\n");
1284
1295
  }
1296
+ function buildReferenceStringList(key, values) {
1297
+ if (values.length === 0) {
1298
+ return [` ${key}: []`];
1299
+ }
1300
+ return [` ${key}:`, ...values.map((value) => ` - ${value}`)];
1301
+ }
1285
1302
  function buildReferencePriorityLines(priority) {
1286
1303
  const lines = [];
1287
1304
  if (priority?.source === "project-field" || priority?.source === "labels") {
@@ -1349,7 +1366,7 @@ function resolveRoleAction(role) {
1349
1366
 
1350
1367
  // src/skills/skill-writer.ts
1351
1368
  import { mkdir as mkdir2, readFile as readFile2, rename, writeFile as writeFile2 } from "fs/promises";
1352
- import { join as join2 } from "path";
1369
+ import { dirname as dirname2, join as join2 } from "path";
1353
1370
  function normalizeRuntimeForSkills(runtime) {
1354
1371
  if (isClaudeRuntime(runtime)) {
1355
1372
  return "claude-code";
@@ -1376,10 +1393,12 @@ function buildSkillFilePlans(repoRoot, runtime, templates, context) {
1376
1393
  }
1377
1394
  return {
1378
1395
  skillsDir,
1379
- files: templates.map((template) => ({
1380
- path: join2(skillsDir, template.name, template.fileName),
1381
- content: template.generate(context)
1382
- }))
1396
+ files: templates.flatMap(
1397
+ (template) => template.files.map((file) => ({
1398
+ path: join2(skillsDir, template.name, file.relativePath),
1399
+ content: file.generate(context)
1400
+ }))
1401
+ )
1383
1402
  };
1384
1403
  }
1385
1404
 
@@ -1419,8 +1438,9 @@ function generateGhSymphonySkill(ctx) {
1419
1438
  `- \`${ctx.contextYamlPath}\` must exist (contains GitHub Project metadata)`
1420
1439
  );
1421
1440
  lines.push(
1422
- `- \`${ctx.referenceWorkflowPath}\` must exist (annotated reference template)`
1441
+ `- \`${ctx.referenceWorkflowPath}\` may exist for compatibility with older generated ecosystems`
1423
1442
  );
1443
+ lines.push("- `references/README.md` must exist beside this skill");
1424
1444
  lines.push("- `gh` CLI must be authenticated");
1425
1445
  lines.push("");
1426
1446
  lines.push("## Repository Validation Guidance");
@@ -1445,49 +1465,70 @@ function generateGhSymphonySkill(ctx) {
1445
1465
  lines.push("## Design Mode");
1446
1466
  lines.push("");
1447
1467
  lines.push(
1448
- `1. Read \`${ctx.contextYamlPath}\` to understand the project structure`
1468
+ "1. Read `WORKFLOW.md` if it exists and extract repository conventions:"
1449
1469
  );
1470
+ lines.push(" - test / lint / build commands from the prompt body");
1471
+ lines.push(" - lifecycle states from front matter");
1450
1472
  lines.push(
1451
- `2. Read \`${ctx.referenceWorkflowPath}\` as the annotated reference`
1473
+ "2. Read `references/README.md` for the available reference files."
1452
1474
  );
1453
- lines.push("3. Ask the user these key questions:");
1475
+ lines.push('3. Ask the user: "What should this orchestration accomplish?"');
1476
+ lines.push(" - implement (default) \u2014 write features / fix bugs");
1477
+ lines.push(" - review \u2014 review PRs and leave comments");
1478
+ lines.push(" - maintain \u2014 dependency bumps, chores, hygiene");
1479
+ lines.push(" - custom \u2014 describe in their own words");
1480
+ lines.push(
1481
+ "4. Pick the matching `references/workflow-posture-*.md` file(s); compose multiple files when the intent spans categories."
1482
+ );
1483
+ lines.push("5. Ask these setup questions:");
1454
1484
  lines.push(" - Which status columns should be **active** (agent works)?");
1455
1485
  lines.push(" - Which should be **wait** (agent pauses for human)?");
1456
1486
  lines.push(" - Which should be **terminal** (agent stops)?");
1457
1487
  lines.push(" - What runtime is being used? (codex / claude-code / custom)");
1458
1488
  lines.push(" - Any custom hooks needed? (after_create, before_run, etc.)");
1489
+ lines.push("6. Generate WORKFLOW.md:");
1490
+ lines.push(" - front matter from `references/workflow-schema.md`");
1459
1491
  lines.push(
1460
- "4. Generate WORKFLOW.md using the reference as a structural guide"
1492
+ " - prompt body from the selected posture file(s), adapted to actual repository commands"
1461
1493
  );
1462
- lines.push("5. Validate the generated file (see Validate Mode)");
1494
+ lines.push("7. Show a diff or preview and confirm with the user.");
1495
+ lines.push("8. Validate via `gh-symphony workflow validate`.");
1463
1496
  lines.push("");
1464
1497
  lines.push("## Refine Mode");
1465
1498
  lines.push("");
1466
1499
  lines.push("1. Read the current `WORKFLOW.md`");
1467
- lines.push(`2. Read \`${ctx.referenceWorkflowPath}\` for comparison`);
1468
- lines.push("3. Identify missing or incomplete sections:");
1500
+ lines.push(
1501
+ "2. Read `references/README.md` and select the relevant posture file(s)"
1502
+ );
1503
+ lines.push(
1504
+ "3. Compare the current prompt body against the selected posture references"
1505
+ );
1506
+ lines.push("4. Identify missing or incomplete sections:");
1469
1507
  lines.push(" - Status Map with role annotations");
1470
1508
  lines.push(" - Default Posture / Agent Instructions");
1471
1509
  lines.push(" - Guardrails section");
1472
1510
  lines.push(" - Workpad Template");
1473
1511
  lines.push(" - Step 0 routing logic");
1474
- lines.push("4. Propose improvements and apply with user confirmation");
1475
- lines.push("5. Validate the refined file");
1512
+ lines.push("5. Propose improvements and apply with user confirmation");
1513
+ lines.push("6. Validate the refined file");
1476
1514
  lines.push("");
1477
1515
  lines.push("## Validate Mode");
1478
1516
  lines.push("");
1479
1517
  lines.push("Check the WORKFLOW.md for:");
1480
1518
  lines.push("- Front matter is valid YAML");
1481
1519
  lines.push(
1482
- "- Required fields are present (see Supported Front Matter Fields)"
1520
+ "- Required fields are present (see `references/workflow-schema.md`)"
1483
1521
  );
1484
1522
  lines.push(
1485
- "- Template variables use only supported names (see Supported Template Variables)"
1523
+ "- Template variables use only supported names (see `references/workflow-schema.md`)"
1486
1524
  );
1487
1525
  lines.push("- Status Map matches the lifecycle configuration");
1488
1526
  lines.push(
1489
1527
  "- No unsupported double-brace variable patterns (only the 8 listed below are valid)"
1490
1528
  );
1529
+ lines.push(
1530
+ "- Prompt body posture is consistent with the selected `references/workflow-posture-*.md` file(s)"
1531
+ );
1491
1532
  lines.push("");
1492
1533
  lines.push("## Supported Front Matter Fields");
1493
1534
  lines.push("");
@@ -1884,22 +1925,319 @@ function generateLandSkill(_ctx) {
1884
1925
  });
1885
1926
  }
1886
1927
 
1928
+ // src/skills/templates/gh-symphony-references/readme.ts
1929
+ function generateGhSymphonyReferencesReadme(_ctx) {
1930
+ return [
1931
+ "# /gh-symphony references",
1932
+ "",
1933
+ "The /gh-symphony skill consults these files when designing or refining",
1934
+ "WORKFLOW.md.",
1935
+ "",
1936
+ "## Schema",
1937
+ "",
1938
+ "| File | What it is |",
1939
+ "| ---- | ---------- |",
1940
+ "| `workflow-schema.md` | All supported front matter fields and their types. |",
1941
+ "",
1942
+ "## Workflow prompt body postures",
1943
+ "",
1944
+ "When the user describes what the orchestration should do, pick the matching",
1945
+ "posture file(s) and use its prompt-body sections as the seed. Postures can be",
1946
+ "combined when the user's intent spans multiple categories.",
1947
+ "",
1948
+ "| File | Use when the user wants... |",
1949
+ "| ---- | -------------------------- |",
1950
+ "| `workflow-posture-implement.md` | Coding agent writes features / bug fixes (default). |",
1951
+ "| `workflow-posture-review.md` | Agent reviews PRs and leaves comments. No code writes. |",
1952
+ "| `workflow-posture-maintain.md` | Minimal-change maintenance: deps, lint sweeps, hygiene. |",
1953
+ "",
1954
+ "## Adding your own reference",
1955
+ "",
1956
+ "Drop a markdown file here with a descriptive name. The skill discovers files",
1957
+ "on each invocation; no code changes needed."
1958
+ ].join("\n");
1959
+ }
1960
+
1961
+ // src/skills/templates/gh-symphony-references/workflow-schema.ts
1962
+ function generateWorkflowSchemaReference(ctx) {
1963
+ const reference = generateReferenceWorkflow({
1964
+ runtime: ctx.runtime,
1965
+ statusColumns: ctx.statusColumns.map((column) => ({
1966
+ name: column.name,
1967
+ role: column.role
1968
+ })),
1969
+ projectId: ctx.projectId,
1970
+ priority: null,
1971
+ detectedEnvironment: ctx.detectedEnvironment
1972
+ });
1973
+ return [
1974
+ reference,
1975
+ "",
1976
+ "## Supported Template Variables",
1977
+ "",
1978
+ "Use these in the WORKFLOW.md prompt body with double-brace syntax.",
1979
+ "",
1980
+ "| Variable | Description |",
1981
+ "| -------- | ----------- |",
1982
+ "| `issue.identifier` | Issue identifier, for example `acme/platform#42`. |",
1983
+ "| `issue.title` | Issue title. |",
1984
+ "| `issue.state` | Current tracker state. |",
1985
+ "| `issue.description` | Issue body. |",
1986
+ "| `issue.url` | Issue URL. |",
1987
+ "| `issue.repository` | Repository in `owner/name` form. |",
1988
+ "| `issue.number` | Issue number. |",
1989
+ "| `attempt` | Retry attempt number, or null on the first run. |",
1990
+ "",
1991
+ "Only these variables are supported by strict-mode prompt rendering."
1992
+ ].join("\n");
1993
+ }
1994
+
1995
+ // src/skills/templates/gh-symphony-references/workflow-posture-implement.ts
1996
+ function generateWorkflowPostureImplementReference(ctx) {
1997
+ const validationGuidance = buildRepositoryValidationGuidance(
1998
+ ctx.detectedEnvironment
1999
+ );
2000
+ return [
2001
+ "# Workflow posture: implement",
2002
+ "",
2003
+ "Use this prompt-body posture when the agent should write features or fix bugs.",
2004
+ "This is the default posture and preserves the current generated WORKFLOW.md",
2005
+ "prompt-body behavior.",
2006
+ "",
2007
+ "## Agent Instructions",
2008
+ "",
2009
+ 'You are an AI coding agent working on issue `{issue.identifier}`: "`{issue.title}`".',
2010
+ "",
2011
+ "**Repository:** `{issue.repository}`",
2012
+ "**Current state:** `{issue.state}`",
2013
+ "",
2014
+ "### Task",
2015
+ "",
2016
+ "`{issue.description}`",
2017
+ "",
2018
+ "### Default Posture",
2019
+ "",
2020
+ "1. This is an unattended orchestration session. Do not ask humans for follow-up actions.",
2021
+ "2. Only abort early if there is a genuine blocker (missing required credentials or secrets).",
2022
+ '3. In your final message, report only what was completed and any blockers. Do not include "next steps".',
2023
+ "",
2024
+ "### Repository Validation Guidance",
2025
+ "",
2026
+ ...validationGuidance.map((line, index) => `${index + 1}. ${line}`),
2027
+ "",
2028
+ "### Workflow",
2029
+ "",
2030
+ "1. Read the issue description and understand the requirements.",
2031
+ "2. Explore the codebase to understand the relevant code structure.",
2032
+ "3. Implement the changes following the project's coding conventions.",
2033
+ "4. Write or update tests to cover the changes.",
2034
+ "5. Verify that all existing tests pass.",
2035
+ "6. Create a PR with a clear description of the changes.",
2036
+ "",
2037
+ "### Guardrails",
2038
+ "",
2039
+ "- Do not edit the issue body for planning or progress tracking.",
2040
+ "- If the issue is in a terminal state, do nothing and exit.",
2041
+ "- If you find out-of-scope improvements, open a separate issue rather than expanding the current scope.",
2042
+ "",
2043
+ "### Workpad Template",
2044
+ "",
2045
+ "Create a workpad comment on the issue with the following structure to track progress:",
2046
+ "",
2047
+ "```md",
2048
+ "## Workpad",
2049
+ "",
2050
+ "### Plan",
2051
+ "",
2052
+ "- [ ] 1. Task item",
2053
+ "",
2054
+ "### Acceptance Criteria",
2055
+ "",
2056
+ "- [ ] Criterion 1",
2057
+ "",
2058
+ "### Validation",
2059
+ "",
2060
+ "- [ ] Test: `command`",
2061
+ "",
2062
+ "### Notes",
2063
+ "",
2064
+ "- Progress notes",
2065
+ "```"
2066
+ ].join("\n");
2067
+ }
2068
+
2069
+ // src/skills/templates/gh-symphony-references/workflow-posture-review.ts
2070
+ function generateWorkflowPostureReviewReference(ctx) {
2071
+ const validationGuidance = buildRepositoryValidationGuidance(
2072
+ ctx.detectedEnvironment
2073
+ );
2074
+ return [
2075
+ "# Workflow posture: review",
2076
+ "",
2077
+ "Use this prompt-body posture when the agent should review PRs and leave",
2078
+ "comments. This posture is read-only for repository code.",
2079
+ "",
2080
+ "## Agent Instructions",
2081
+ "",
2082
+ 'You are an AI code-review agent working on issue `{issue.identifier}`: "`{issue.title}`".',
2083
+ "",
2084
+ "**Repository:** `{issue.repository}`",
2085
+ "**Current state:** `{issue.state}`",
2086
+ "",
2087
+ "### Task",
2088
+ "",
2089
+ "`{issue.description}`",
2090
+ "",
2091
+ "### Default Posture",
2092
+ "",
2093
+ "1. Review linked pull requests. Do NOT write code, push commits, or open new PRs.",
2094
+ "2. Treat failing required tests as grounds to request changes unless the failure is clearly unrelated and documented.",
2095
+ "3. In your final message, report only the review outcome and any blockers. Do not include follow-up work for the human unless it is required to unblock review.",
2096
+ "",
2097
+ "### Repository Validation Guidance",
2098
+ "",
2099
+ ...validationGuidance.map((line, index) => `${index + 1}. ${line}`),
2100
+ "",
2101
+ "### Workflow",
2102
+ "",
2103
+ "1. Find the PR linked from the issue, project item, or issue timeline.",
2104
+ "2. Read the PR title, body, diff, linked issue, existing reviews, inline comments, and check status.",
2105
+ "3. Run the repository's relevant tests, lint, typecheck, or build commands when available and practical.",
2106
+ "4. Leave inline review comments for concrete, actionable findings.",
2107
+ "5. Submit a summary review: approve only when the change is correct and validation is acceptable; otherwise request changes.",
2108
+ "",
2109
+ "### Guardrails",
2110
+ "",
2111
+ "- Never push code from this posture.",
2112
+ "- Never approve PRs that introduce new dependencies without explicitly noting the dependency risk and why it is acceptable.",
2113
+ "- If relevant tests fail and the failure is not proven unrelated, request changes.",
2114
+ "- Keep comments specific to correctness, maintainability, tests, security, and issue fit.",
2115
+ "- Do not create a workpad; the review threads on the PR are the audit trail."
2116
+ ].join("\n");
2117
+ }
2118
+
2119
+ // src/skills/templates/gh-symphony-references/workflow-posture-maintain.ts
2120
+ function generateWorkflowPostureMaintainReference(ctx) {
2121
+ const validationGuidance = buildRepositoryValidationGuidance(
2122
+ ctx.detectedEnvironment
2123
+ );
2124
+ return [
2125
+ "# Workflow posture: maintain",
2126
+ "",
2127
+ "Use this prompt-body posture for low-risk maintenance such as dependency",
2128
+ "bumps, lint sweeps, small chores, and repository hygiene.",
2129
+ "",
2130
+ "## Agent Instructions",
2131
+ "",
2132
+ 'You are a maintenance coding agent working on issue `{issue.identifier}`: "`{issue.title}`".',
2133
+ "",
2134
+ "**Repository:** `{issue.repository}`",
2135
+ "**Current state:** `{issue.state}`",
2136
+ "",
2137
+ "### Task",
2138
+ "",
2139
+ "`{issue.description}`",
2140
+ "",
2141
+ "### Default Posture",
2142
+ "",
2143
+ "1. Make the smallest possible change that satisfies the maintenance request.",
2144
+ "2. Defer human-judgment calls instead of broadening scope.",
2145
+ "3. In your final message, report only what was completed and any blockers. Do not include optional next steps.",
2146
+ "",
2147
+ "### Repository Validation Guidance",
2148
+ "",
2149
+ ...validationGuidance.map((line, index) => `${index + 1}. ${line}`),
2150
+ "",
2151
+ "### Workflow",
2152
+ "",
2153
+ "1. Identify the exact maintenance task and affected files.",
2154
+ "2. Make the minimal change needed; avoid drive-by refactors.",
2155
+ "3. Run the relevant tests, lint, typecheck, or build commands for the affected area.",
2156
+ "4. Create a PR when the change is complete, or exit with a blocker note if approval is required.",
2157
+ "",
2158
+ "### Guardrails",
2159
+ "",
2160
+ "- Do not perform major dependency bumps without explicit approval.",
2161
+ "- Do not delete files without confirmation unless the issue explicitly requests it.",
2162
+ "- If the implementation exceeds 50 lines of non-generated code, stop and ask for human confirmation before continuing.",
2163
+ "- Do not mix unrelated cleanup into the maintenance change.",
2164
+ "",
2165
+ "### Workpad Template",
2166
+ "",
2167
+ "Create a compact workpad comment on the issue with the following structure:",
2168
+ "",
2169
+ "```md",
2170
+ "## Workpad",
2171
+ "",
2172
+ "### Plan",
2173
+ "",
2174
+ "- [ ] Minimal maintenance change",
2175
+ "- [ ] Validation and PR handoff",
2176
+ "",
2177
+ "### Validation",
2178
+ "",
2179
+ "- [ ] Test/lint/typecheck/build command",
2180
+ "",
2181
+ "### Blockers",
2182
+ "",
2183
+ "None",
2184
+ "```"
2185
+ ].join("\n");
2186
+ }
2187
+
2188
+ // src/skills/templates/gh-symphony-references/index.ts
2189
+ var GH_SYMPHONY_REFERENCE_FILES = [
2190
+ {
2191
+ relativePath: "references/README.md",
2192
+ generate: generateGhSymphonyReferencesReadme
2193
+ },
2194
+ {
2195
+ relativePath: "references/workflow-schema.md",
2196
+ generate: generateWorkflowSchemaReference
2197
+ },
2198
+ {
2199
+ relativePath: "references/workflow-posture-implement.md",
2200
+ generate: generateWorkflowPostureImplementReference
2201
+ },
2202
+ {
2203
+ relativePath: "references/workflow-posture-review.md",
2204
+ generate: generateWorkflowPostureReviewReference
2205
+ },
2206
+ {
2207
+ relativePath: "references/workflow-posture-maintain.md",
2208
+ generate: generateWorkflowPostureMaintainReference
2209
+ }
2210
+ ];
2211
+
1887
2212
  // src/skills/templates/index.ts
1888
2213
  var ALL_SKILL_TEMPLATES = [
1889
2214
  {
1890
2215
  name: "gh-symphony",
1891
- fileName: "SKILL.md",
1892
- generate: generateGhSymphonySkill
2216
+ files: [
2217
+ { relativePath: "SKILL.md", generate: generateGhSymphonySkill },
2218
+ ...GH_SYMPHONY_REFERENCE_FILES
2219
+ ]
1893
2220
  },
1894
2221
  {
1895
2222
  name: "gh-project",
1896
- fileName: "SKILL.md",
1897
- generate: generateGhProjectSkill
2223
+ files: [{ relativePath: "SKILL.md", generate: generateGhProjectSkill }]
2224
+ },
2225
+ {
2226
+ name: "commit",
2227
+ files: [{ relativePath: "SKILL.md", generate: generateCommitSkill }]
2228
+ },
2229
+ {
2230
+ name: "push",
2231
+ files: [{ relativePath: "SKILL.md", generate: generatePushSkill }]
2232
+ },
2233
+ {
2234
+ name: "pull",
2235
+ files: [{ relativePath: "SKILL.md", generate: generatePullSkill }]
1898
2236
  },
1899
- { name: "commit", fileName: "SKILL.md", generate: generateCommitSkill },
1900
- { name: "push", fileName: "SKILL.md", generate: generatePushSkill },
1901
- { name: "pull", fileName: "SKILL.md", generate: generatePullSkill },
1902
- { name: "land", fileName: "SKILL.md", generate: generateLandSkill }
2237
+ {
2238
+ name: "land",
2239
+ files: [{ relativePath: "SKILL.md", generate: generateLandSkill }]
2240
+ }
1903
2241
  ];
1904
2242
 
1905
2243
  // src/commands/workflow-init.ts
@@ -2017,7 +2355,7 @@ function validateInitRuntime(runtime) {
2017
2355
  async function promptRuntimeSelection() {
2018
2356
  return abortIfCancelled(
2019
2357
  p.select({
2020
- message: "Step 1/3 \u2014 Select the agent runtime:",
2358
+ message: "Step 1/5 \u2014 Select the agent runtime:",
2021
2359
  options: [
2022
2360
  {
2023
2361
  value: "codex-app-server",
@@ -2095,7 +2433,7 @@ async function writePlannedFile(file) {
2095
2433
  if (file.status === "unchanged") {
2096
2434
  return false;
2097
2435
  }
2098
- await mkdir3(dirname2(file.path), { recursive: true });
2436
+ await mkdir3(dirname3(file.path), { recursive: true });
2099
2437
  const temporaryPath = `${file.path}.tmp`;
2100
2438
  await writeFile3(temporaryPath, file.content, "utf8");
2101
2439
  await rename2(temporaryPath, file.path);
@@ -2104,6 +2442,9 @@ async function writePlannedFile(file) {
2104
2442
  }
2105
2443
  return true;
2106
2444
  }
2445
+ function skillNameForPath(skillsDir, filePath) {
2446
+ return relative(skillsDir, filePath).split(/[\\/]/)[0] ?? "";
2447
+ }
2107
2448
  function resolveStatusField(projectDetail) {
2108
2449
  return projectDetail.statusFields.find((f) => f.name.toLowerCase() === "status") ?? projectDetail.statusFields[0] ?? null;
2109
2450
  }
@@ -2318,16 +2659,68 @@ async function promptStateMappings(statusField, options) {
2318
2659
  }
2319
2660
  return mappings;
2320
2661
  }
2662
+ function getDefaultBlockerCheckStates(lifecycle) {
2663
+ const firstActive = lifecycle.activeStates[0];
2664
+ return firstActive ? [firstActive] : [];
2665
+ }
2666
+ async function promptBlockerCheck(lifecycle, options) {
2667
+ const stepLabel = options?.stepLabel ?? "Step 3/5";
2668
+ const activeStates = lifecycle.activeStates;
2669
+ const defaultStates = getDefaultBlockerCheckStates(lifecycle);
2670
+ if (activeStates.length === 0) {
2671
+ p.log.warn("No active states; blocker check cannot be enabled.");
2672
+ p.log.info("Blocker check: disabled");
2673
+ return [];
2674
+ }
2675
+ const activeStateSummary = activeStates.length === 1 ? `"${activeStates[0]}"` : "selected active states";
2676
+ const enabled = await abortIfCancelled(
2677
+ p.confirm({
2678
+ message: `${stepLabel} \u2014 Enable blocker check? Issues with unresolved "blocked by" dependencies will be held back from dispatch on ${activeStateSummary}.`,
2679
+ initialValue: true
2680
+ })
2681
+ );
2682
+ if (!enabled) {
2683
+ p.log.info("Blocker check: disabled");
2684
+ return [];
2685
+ }
2686
+ if (activeStates.length === 1) {
2687
+ p.log.info(`Blocker check applies to: ${activeStates[0]}`);
2688
+ return [activeStates[0]];
2689
+ }
2690
+ const selectedStates = await abortIfCancelled(
2691
+ p.multiselect({
2692
+ message: `${stepLabel} \u2014 Which active states should be blocker-checked?`,
2693
+ options: activeStates.map((state) => ({
2694
+ value: state,
2695
+ label: state,
2696
+ hint: defaultStates.includes(state) ? "default" : void 0
2697
+ })),
2698
+ initialValues: defaultStates,
2699
+ required: true
2700
+ })
2701
+ );
2702
+ p.log.info(`Blocker check applies to: ${selectedStates.join(", ")}`);
2703
+ return [...selectedStates];
2704
+ }
2321
2705
  async function planWorkflowArtifacts(opts) {
2322
2706
  const environment = opts.environment ?? await detectEnvironment(opts.cwd);
2323
2707
  const priority = opts.priority ?? (opts.priorityField ? buildProjectFieldPriority(opts.priorityField) : buildDisabledPriority());
2708
+ const defaultLifecycle = toWorkflowLifecycleConfig(
2709
+ opts.statusField.name,
2710
+ opts.mappings
2711
+ );
2712
+ const defaultBlockerCheckStates = getDefaultBlockerCheckStates(defaultLifecycle);
2713
+ const lifecycle = opts.lifecycle ?? toWorkflowLifecycleConfig(opts.statusField.name, opts.mappings, {
2714
+ blockerCheckStates: defaultBlockerCheckStates,
2715
+ planningStates: defaultBlockerCheckStates
2716
+ });
2324
2717
  const workflowMd = generateWorkflowMarkdown({
2325
2718
  projectId: opts.projectDetail.id,
2326
2719
  stateFieldName: opts.statusField.name,
2327
2720
  priority,
2328
2721
  includePriorityTemplates: opts.includePriorityTemplates ?? priority.source === "disabled",
2329
2722
  mappings: opts.mappings,
2330
- lifecycle: toWorkflowLifecycleConfig(opts.statusField.name, opts.mappings),
2723
+ lifecycle,
2331
2724
  runtime: opts.runtime,
2332
2725
  detectedEnvironment: environment
2333
2726
  });
@@ -2343,6 +2736,7 @@ async function planWorkflowArtifacts(opts) {
2343
2736
  statusField: opts.statusField,
2344
2737
  priorityField: opts.priorityField,
2345
2738
  priority,
2739
+ lifecycle,
2346
2740
  includePriorityTemplates: opts.includePriorityTemplates ?? priority.source === "disabled",
2347
2741
  runtime: opts.runtime,
2348
2742
  skipSkills: opts.skipSkills,
@@ -2368,6 +2762,11 @@ function summarizeEnvironment(env) {
2368
2762
  `Existing skills ${env.existingSkills.length === 0 ? "none" : env.existingSkills.join(", ")}`
2369
2763
  ];
2370
2764
  }
2765
+ function deriveWaitStates(statusField, lifecycle) {
2766
+ const active = new Set(lifecycle.activeStates);
2767
+ const terminal = new Set(lifecycle.terminalStates);
2768
+ return statusField.options.map((option) => option.name).filter((state) => !active.has(state) && !terminal.has(state));
2769
+ }
2371
2770
  async function planEcosystem(opts) {
2372
2771
  const {
2373
2772
  cwd,
@@ -2379,6 +2778,19 @@ async function planEcosystem(opts) {
2379
2778
  skipContext
2380
2779
  } = opts;
2381
2780
  const priority = opts.priority ?? (priorityField ? buildProjectFieldPriority(priorityField) : buildDisabledPriority());
2781
+ const automaticLifecycle = toWorkflowLifecycleConfig(
2782
+ statusField.name,
2783
+ buildAutomaticStateMappings(statusField)
2784
+ );
2785
+ const defaultBlockerCheckStates = getDefaultBlockerCheckStates(automaticLifecycle);
2786
+ const lifecycle = opts.lifecycle ?? toWorkflowLifecycleConfig(
2787
+ statusField.name,
2788
+ buildAutomaticStateMappings(statusField),
2789
+ {
2790
+ blockerCheckStates: defaultBlockerCheckStates,
2791
+ planningStates: defaultBlockerCheckStates
2792
+ }
2793
+ );
2382
2794
  const ghSymphonyDir = join3(cwd, ".gh-symphony");
2383
2795
  const environment = opts.environment ?? await detectEnvironment(cwd);
2384
2796
  const files = [];
@@ -2418,6 +2830,7 @@ async function planEcosystem(opts) {
2418
2830
  })),
2419
2831
  projectId: projectDetail.id,
2420
2832
  priority,
2833
+ lifecycle,
2421
2834
  detectedEnvironment: environment
2422
2835
  });
2423
2836
  files.push(
@@ -2454,10 +2867,11 @@ async function planEcosystem(opts) {
2454
2867
  }
2455
2868
  );
2456
2869
  for (const plannedSkill of plannedSkills) {
2870
+ const skillName = skillNameForPath(skillsDir, plannedSkill.path);
2457
2871
  files.push(
2458
2872
  await planFileChange({
2459
2873
  path: plannedSkill.path,
2460
- label: `Skill ${basename(dirname2(plannedSkill.path))}`,
2874
+ label: `Skill ${skillName}`,
2461
2875
  content: plannedSkill.content,
2462
2876
  mode: "create-only"
2463
2877
  })
@@ -2469,6 +2883,8 @@ async function planEcosystem(opts) {
2469
2883
  githubProjectTitle: projectDetail.title,
2470
2884
  runtime,
2471
2885
  priority,
2886
+ lifecycle,
2887
+ waitStates: deriveWaitStates(statusField, lifecycle),
2472
2888
  skillsDir,
2473
2889
  skipSkills,
2474
2890
  environment,
@@ -2505,7 +2921,7 @@ async function writeEcosystem(opts) {
2505
2921
  continue;
2506
2922
  }
2507
2923
  if (file.label.startsWith("Skill ")) {
2508
- const skillName = basename(dirname2(file.path));
2924
+ const skillName = file.label.slice("Skill ".length);
2509
2925
  if (written) {
2510
2926
  skillsWritten.push(skillName);
2511
2927
  } else {
@@ -2518,13 +2934,15 @@ async function writeEcosystem(opts) {
2518
2934
  githubProjectTitle: plan.githubProjectTitle,
2519
2935
  runtime: plan.runtime,
2520
2936
  priority: plan.priority,
2937
+ lifecycle: plan.lifecycle,
2938
+ waitStates: plan.waitStates,
2521
2939
  skillsDir: plan.skillsDir,
2522
2940
  skipSkills: plan.skipSkills,
2523
2941
  afterCreateHookWritten,
2524
2942
  contextYamlWritten,
2525
2943
  referenceWorkflowWritten,
2526
- skillsWritten: skillsWritten.sort(),
2527
- skillsSkipped: skillsSkipped.sort()
2944
+ skillsWritten: [...new Set(skillsWritten)].sort(),
2945
+ skillsSkipped: [...new Set(skillsSkipped)].sort()
2528
2946
  };
2529
2947
  }
2530
2948
  function formatPrioritySummaryLines(priority) {
@@ -2539,9 +2957,20 @@ function formatPrioritySummaryLines(priority) {
2539
2957
  ];
2540
2958
  }
2541
2959
  const mapping = Object.entries(priority.labels).map(([name, value]) => `${name}=${value}`).join(", ");
2960
+ return ["Priority source labels", `Priority mapping ${mapping || "none"}`];
2961
+ }
2962
+ function formatLifecycleValue(states) {
2963
+ return states.length > 0 ? states.join(", ") : "disabled";
2964
+ }
2965
+ function formatLifecycleSummaryLines(lifecycle, waitStates) {
2542
2966
  return [
2543
- "Priority source labels",
2544
- `Priority mapping ${mapping || "none"}`
2967
+ "Lifecycle",
2968
+ ` Status field ${lifecycle.stateFieldName}`,
2969
+ ` Active ${lifecycle.activeStates.join(", ") || "(none)"}`,
2970
+ ` Wait ${waitStates.join(", ") || "(none)"}`,
2971
+ ` Terminal ${lifecycle.terminalStates.join(", ") || "(none)"}`,
2972
+ ` Blocker check ${formatLifecycleValue(lifecycle.blockerCheckStates)}`,
2973
+ ` Planning ${formatLifecycleValue(lifecycle.planningStates)}`
2545
2974
  ];
2546
2975
  }
2547
2976
  function printEcosystemSummary(result, workflowPath, opts) {
@@ -2554,6 +2983,10 @@ function printEcosystemSummary(result, workflowPath, opts) {
2554
2983
  lines.push(`Runtime ${result.runtime}`);
2555
2984
  lines.push(...formatPrioritySummaryLines(result.priority));
2556
2985
  lines.push("");
2986
+ lines.push(
2987
+ ...formatLifecycleSummaryLines(result.lifecycle, result.waitStates)
2988
+ );
2989
+ lines.push("");
2557
2990
  lines.push("Generated files");
2558
2991
  lines.push(` \u2713 WORKFLOW.md ${relWorkflow}`);
2559
2992
  if (result.afterCreateHookWritten) {
@@ -2608,6 +3041,13 @@ function renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
2608
3041
  lines.push(`Runtime ${ecosystemPlan.runtime}`);
2609
3042
  lines.push(...formatPrioritySummaryLines(ecosystemPlan.priority));
2610
3043
  lines.push("");
3044
+ lines.push(
3045
+ ...formatLifecycleSummaryLines(
3046
+ ecosystemPlan.lifecycle,
3047
+ ecosystemPlan.waitStates
3048
+ )
3049
+ );
3050
+ lines.push("");
2611
3051
  lines.push("Planned file changes");
2612
3052
  lines.push(
2613
3053
  ` ${statusIcon[workflowPlan.status]} ${workflowPlan.status.padEnd(9)} WORKFLOW.md ${relWorkflow}`
@@ -2737,6 +3177,13 @@ Run without --non-interactive for manual mapping.
2737
3177
  process.exitCode = 1;
2738
3178
  return;
2739
3179
  }
3180
+ const defaultBlockerCheckStates = getDefaultBlockerCheckStates(
3181
+ toWorkflowLifecycleConfig(statusField.name, mappings)
3182
+ );
3183
+ const lifecycle = toWorkflowLifecycleConfig(statusField.name, mappings, {
3184
+ blockerCheckStates: defaultBlockerCheckStates,
3185
+ planningStates: defaultBlockerCheckStates
3186
+ });
2740
3187
  const outputPath = resolve(flags.output ?? "WORKFLOW.md");
2741
3188
  const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
2742
3189
  cwd: process.cwd(),
@@ -2747,6 +3194,7 @@ Run without --non-interactive for manual mapping.
2747
3194
  priority,
2748
3195
  includePriorityTemplates: !autoPriorityField,
2749
3196
  mappings,
3197
+ lifecycle,
2750
3198
  runtime,
2751
3199
  skipSkills: flags.skipSkills,
2752
3200
  skipContext: flags.skipContext
@@ -2771,6 +3219,7 @@ Run without --non-interactive for manual mapping.
2771
3219
  priorityField: autoPriorityField,
2772
3220
  priority,
2773
3221
  includePriorityTemplates: !autoPriorityField,
3222
+ lifecycle,
2774
3223
  runtime,
2775
3224
  skipSkills: flags.skipSkills,
2776
3225
  skipContext: flags.skipContext
@@ -2847,7 +3296,7 @@ async function runInteractiveStandalone(flags, _options) {
2847
3296
  }
2848
3297
  const selectedGithubProjectId = await abortIfCancelled(
2849
3298
  p.select({
2850
- message: "Step 2/3 \u2014 Select a GitHub Project board:",
3299
+ message: "Step 2/5 \u2014 Select a GitHub Project board:",
2851
3300
  options: projects.map((proj) => ({
2852
3301
  value: proj.id,
2853
3302
  label: `${proj.owner.login}/${proj.title}`,
@@ -2882,12 +3331,7 @@ async function runInteractiveStandalone(flags, _options) {
2882
3331
  projectDetail.linkedRepositories
2883
3332
  );
2884
3333
  const mappings = await promptStateMappings(statusField, {
2885
- stepLabel: "Step 3/4"
2886
- });
2887
- const { priority, priorityField } = await promptPriorityConfig({
2888
- priorityResolution,
2889
- labelNames: priorityLabelNames,
2890
- stepLabel: "Step 4/4"
3334
+ stepLabel: "Step 3/5"
2891
3335
  });
2892
3336
  const validation = validateStateMapping(mappings);
2893
3337
  if (!validation.valid) {
@@ -2901,6 +3345,19 @@ async function runInteractiveStandalone(flags, _options) {
2901
3345
  for (const warn of validation.warnings) {
2902
3346
  p.log.warn(` \u26A0 ${warn}`);
2903
3347
  }
3348
+ const lifecycleBase = toWorkflowLifecycleConfig(statusField.name, mappings);
3349
+ const blockerCheckStates = await promptBlockerCheck(lifecycleBase, {
3350
+ stepLabel: "Step 4/5"
3351
+ });
3352
+ const lifecycle = toWorkflowLifecycleConfig(statusField.name, mappings, {
3353
+ blockerCheckStates,
3354
+ planningStates: blockerCheckStates
3355
+ });
3356
+ const { priority, priorityField } = await promptPriorityConfig({
3357
+ priorityResolution,
3358
+ labelNames: priorityLabelNames,
3359
+ stepLabel: "Step 5/5"
3360
+ });
2904
3361
  const outputPath = resolve(flags.output ?? "WORKFLOW.md");
2905
3362
  const { workflowPlan, ecosystemPlan } = await planWorkflowArtifacts({
2906
3363
  cwd: process.cwd(),
@@ -2911,6 +3368,7 @@ async function runInteractiveStandalone(flags, _options) {
2911
3368
  priority,
2912
3369
  includePriorityTemplates: priority.source === "disabled",
2913
3370
  mappings,
3371
+ lifecycle,
2914
3372
  runtime,
2915
3373
  skipSkills: flags.skipSkills,
2916
3374
  skipContext: flags.skipContext
@@ -2927,6 +3385,7 @@ async function runInteractiveStandalone(flags, _options) {
2927
3385
  priorityField,
2928
3386
  priority,
2929
3387
  includePriorityTemplates: priority.source === "disabled",
3388
+ lifecycle,
2930
3389
  runtime,
2931
3390
  skipSkills: flags.skipSkills,
2932
3391
  skipContext: flags.skipContext
@@ -2938,6 +3397,7 @@ async function runInteractiveStandalone(flags, _options) {
2938
3397
  }
2939
3398
 
2940
3399
  export {
3400
+ toWorkflowLifecycleConfig,
2941
3401
  validateStateMapping,
2942
3402
  abortIfCancelled,
2943
3403
  workflow_init_default,
@@ -2947,6 +3407,7 @@ export {
2947
3407
  collectPriorityLabelNames,
2948
3408
  promptPriorityConfig,
2949
3409
  promptStateMappings,
3410
+ promptBlockerCheck,
2950
3411
  planWorkflowArtifacts,
2951
3412
  writeWorkflowPlan,
2952
3413
  writeEcosystem,