@gh-symphony/cli 0.2.0 → 0.2.3

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.
@@ -7,15 +7,16 @@ import {
7
7
  discoverUserProjects,
8
8
  getGhTokenWithSource,
9
9
  getProjectDetail,
10
+ listRepositoryLabels,
10
11
  listUserProjects,
11
12
  resolveGitHubAuth,
12
13
  validateToken
13
- } from "./chunk-Z3NZOPLZ.js";
14
+ } from "./chunk-BOM2BYZQ.js";
14
15
  import {
15
16
  formatClaudePreflightText,
16
17
  resolveClaudeCommandBinary,
17
18
  runClaudePreflight
18
- } from "./chunk-Q3UEPUE3.js";
19
+ } from "./chunk-3SKN5L3I.js";
19
20
 
20
21
  // src/mapping/smart-defaults.ts
21
22
  var ROLE_PATTERNS = [
@@ -347,9 +348,7 @@ function buildFrontMatter(input) {
347
348
  lines.push(" kind: github-project");
348
349
  lines.push(` project_id: ${input.projectId}`);
349
350
  lines.push(` state_field: ${input.stateFieldName}`);
350
- if (input.priorityFieldName) {
351
- lines.push(` priority_field: ${input.priorityFieldName}`);
352
- }
351
+ lines.push(...buildPriorityFrontMatter(input));
353
352
  if (input.lifecycle.activeStates.length > 0) {
354
353
  lines.push(" active_states:");
355
354
  for (const state of input.lifecycle.activeStates) {
@@ -381,6 +380,67 @@ function buildFrontMatter(input) {
381
380
  lines.push(...buildRuntimeFrontMatter(input.runtime));
382
381
  return lines.join("\n") + "\n";
383
382
  }
383
+ function buildPriorityFrontMatter(input) {
384
+ const lines = [];
385
+ if (!input.priority) {
386
+ return lines;
387
+ }
388
+ if (input.priority.source === "disabled") {
389
+ lines.push(
390
+ " # Priority dispatch is disabled until an operator chooses one explicit source."
391
+ );
392
+ } else {
393
+ lines.push(
394
+ " # Priority is explicit. Numbers below are editable policy (lower = higher priority)."
395
+ );
396
+ }
397
+ lines.push(
398
+ " # See docs/adr/2026-05-18_explicit-dispatch-priority-mappings.md"
399
+ );
400
+ lines.push(" priority:");
401
+ if (input.priority.source === "project-field") {
402
+ lines.push(" source: project-field");
403
+ lines.push(` field: ${formatYamlScalar(input.priority.field)}`);
404
+ lines.push(" values:");
405
+ for (const [name, value] of Object.entries(input.priority.values)) {
406
+ lines.push(` ${formatYamlKey(name)}: ${value}`);
407
+ }
408
+ return lines;
409
+ }
410
+ if (input.priority.source === "labels") {
411
+ lines.push(" source: labels");
412
+ lines.push(" labels:");
413
+ for (const [name, value] of Object.entries(input.priority.labels)) {
414
+ lines.push(` ${formatYamlKey(name)}: ${value}`);
415
+ }
416
+ return lines;
417
+ }
418
+ lines.push(" source: disabled");
419
+ if (input.includePriorityTemplates) {
420
+ lines.push("");
421
+ lines.push(" # Optional template: project-field priority source.");
422
+ lines.push(" # priority:");
423
+ lines.push(" # source: project-field");
424
+ lines.push(" # field: Priority");
425
+ lines.push(" # values:");
426
+ lines.push(" # Urgent: 0");
427
+ lines.push(" # High: 1");
428
+ lines.push("");
429
+ lines.push(" # Optional template: labels priority source.");
430
+ lines.push(" # priority:");
431
+ lines.push(" # source: labels");
432
+ lines.push(" # labels:");
433
+ lines.push(" # P0: 0");
434
+ lines.push(" # P1: 1");
435
+ }
436
+ return lines;
437
+ }
438
+ function formatYamlScalar(value) {
439
+ return JSON.stringify(value);
440
+ }
441
+ function formatYamlKey(value) {
442
+ return JSON.stringify(value);
443
+ }
384
444
  function buildPromptBody(input) {
385
445
  const statusMap = generateStatusMapWithDescriptions(input.mappings);
386
446
  const validationGuidance = buildRepositoryValidationGuidance(
@@ -917,11 +977,7 @@ function generateReferenceWorkflow(input) {
917
977
  lines.push(" kind: github-project");
918
978
  lines.push(` project_id: ${input.projectId}`);
919
979
  lines.push(" state_field: Status");
920
- if (input.priorityFieldName) {
921
- lines.push(` priority_field: ${input.priorityFieldName}`);
922
- } else {
923
- lines.push(" # priority_field: Priority");
924
- }
980
+ lines.push(...buildReferencePriorityLines(input.priority));
925
981
  lines.push("");
926
982
  const activeColumns = input.statusColumns.filter((c) => c.role === "active");
927
983
  const waitColumns = input.statusColumns.filter((c) => c.role === "wait");
@@ -1226,6 +1282,58 @@ function generateReferenceWorkflow(input) {
1226
1282
  lines.push("");
1227
1283
  return lines.join("\n");
1228
1284
  }
1285
+ function buildReferencePriorityLines(priority) {
1286
+ const lines = [];
1287
+ if (priority?.source === "project-field" || priority?.source === "labels") {
1288
+ lines.push(
1289
+ " # Priority is explicit. Numbers below are editable policy (lower = higher priority)."
1290
+ );
1291
+ } else {
1292
+ lines.push(
1293
+ " # Priority dispatch is disabled until an operator chooses one explicit source."
1294
+ );
1295
+ }
1296
+ lines.push(
1297
+ " # See docs/adr/2026-05-18_explicit-dispatch-priority-mappings.md"
1298
+ );
1299
+ if (priority?.source === "project-field") {
1300
+ lines.push(" priority:");
1301
+ lines.push(" source: project-field");
1302
+ lines.push(` field: ${JSON.stringify(priority.field)}`);
1303
+ lines.push(" values:");
1304
+ for (const [name, value] of Object.entries(priority.values)) {
1305
+ lines.push(` ${JSON.stringify(name)}: ${value}`);
1306
+ }
1307
+ return lines;
1308
+ }
1309
+ if (priority?.source === "labels") {
1310
+ lines.push(" priority:");
1311
+ lines.push(" source: labels");
1312
+ lines.push(" labels:");
1313
+ for (const [name, value] of Object.entries(priority.labels)) {
1314
+ lines.push(` ${JSON.stringify(name)}: ${value}`);
1315
+ }
1316
+ return lines;
1317
+ }
1318
+ lines.push(" priority:");
1319
+ lines.push(" source: disabled");
1320
+ lines.push("");
1321
+ lines.push(" # Optional template: project-field priority source.");
1322
+ lines.push(" # priority:");
1323
+ lines.push(" # source: project-field");
1324
+ lines.push(" # field: Priority");
1325
+ lines.push(" # values:");
1326
+ lines.push(" # Urgent: 0");
1327
+ lines.push(" # High: 1");
1328
+ lines.push("");
1329
+ lines.push(" # Optional template: labels priority source.");
1330
+ lines.push(" # priority:");
1331
+ lines.push(" # source: labels");
1332
+ lines.push(" # labels:");
1333
+ lines.push(" # P0: 0");
1334
+ lines.push(" # P1: 1");
1335
+ return lines;
1336
+ }
1229
1337
  function resolveRoleAction(role) {
1230
1338
  switch (role) {
1231
1339
  case "active":
@@ -2037,6 +2145,35 @@ function resolvePriorityField(projectDetail, statusField) {
2037
2145
  }
2038
2146
  return { field: null, ambiguous: [] };
2039
2147
  }
2148
+ function buildProjectFieldPriority(field) {
2149
+ return {
2150
+ source: "project-field",
2151
+ field: field.name,
2152
+ values: Object.fromEntries(
2153
+ field.options.map((option, index) => [option.name, index])
2154
+ )
2155
+ };
2156
+ }
2157
+ function buildDisabledPriority() {
2158
+ return { source: "disabled" };
2159
+ }
2160
+ async function collectPriorityLabelNames(client, repositories) {
2161
+ const labels = /* @__PURE__ */ new Set();
2162
+ for (const repository of repositories) {
2163
+ try {
2164
+ const repoLabels = await listRepositoryLabels(
2165
+ client,
2166
+ repository.owner,
2167
+ repository.name
2168
+ );
2169
+ for (const label of repoLabels) {
2170
+ labels.add(label.name);
2171
+ }
2172
+ } catch {
2173
+ }
2174
+ }
2175
+ return [...labels].sort((a, b) => a.localeCompare(b));
2176
+ }
2040
2177
  async function promptPriorityField(priorityCandidates, options) {
2041
2178
  if (priorityCandidates.length === 0) {
2042
2179
  return null;
@@ -2053,7 +2190,7 @@ async function promptPriorityField(priorityCandidates, options) {
2053
2190
  {
2054
2191
  value: "__skip_priority_field__",
2055
2192
  label: "Skip priority-aware dispatch",
2056
- hint: "Leave tracker.priority_field unset"
2193
+ hint: "Write source: disabled"
2057
2194
  }
2058
2195
  ]
2059
2196
  })
@@ -2063,6 +2200,97 @@ async function promptPriorityField(priorityCandidates, options) {
2063
2200
  }
2064
2201
  return priorityCandidates.find((field) => field.id === selectedFieldId) ?? null;
2065
2202
  }
2203
+ async function promptProjectFieldPriorityValues(field) {
2204
+ const values = {};
2205
+ for (const [index, option] of field.options.entries()) {
2206
+ const rawValue = await abortIfCancelled(
2207
+ p.text({
2208
+ message: `Priority value for option "${option.name}"`,
2209
+ placeholder: String(index),
2210
+ initialValue: String(index),
2211
+ validate: validatePriorityInteger
2212
+ })
2213
+ );
2214
+ values[option.name] = Number(rawValue);
2215
+ }
2216
+ return values;
2217
+ }
2218
+ async function promptPriorityConfig(input) {
2219
+ const hasProjectField = Boolean(input.priorityResolution.field) || input.priorityResolution.ambiguous.length > 0;
2220
+ const hasLabels = input.labelNames.length > 0;
2221
+ const selectedSource = await abortIfCancelled(
2222
+ p.select({
2223
+ message: `${input.stepLabel ?? "Priority"} \u2014 Choose one priority source:`,
2224
+ options: [
2225
+ ...hasProjectField ? [
2226
+ {
2227
+ value: "project-field",
2228
+ label: "GitHub Project field",
2229
+ hint: "Map single-select options to explicit numbers"
2230
+ }
2231
+ ] : [],
2232
+ ...hasLabels ? [
2233
+ {
2234
+ value: "labels",
2235
+ label: "GitHub labels",
2236
+ hint: "Map existing repository labels to explicit numbers"
2237
+ }
2238
+ ] : [],
2239
+ {
2240
+ value: "disabled",
2241
+ label: "Disabled",
2242
+ hint: "Write source: disabled"
2243
+ }
2244
+ ]
2245
+ })
2246
+ );
2247
+ if (selectedSource === "disabled") {
2248
+ return { priority: buildDisabledPriority(), priorityField: null };
2249
+ }
2250
+ if (selectedSource === "labels") {
2251
+ const selectedLabels = await abortIfCancelled(
2252
+ p.multiselect({
2253
+ message: "Select priority labels to map:",
2254
+ options: input.labelNames.map((label) => ({
2255
+ value: label,
2256
+ label
2257
+ })),
2258
+ required: true
2259
+ })
2260
+ );
2261
+ const labels = {};
2262
+ for (const [index, label] of selectedLabels.entries()) {
2263
+ const rawValue = await abortIfCancelled(
2264
+ p.text({
2265
+ message: `Priority value for label "${label}"`,
2266
+ placeholder: String(index),
2267
+ initialValue: String(index),
2268
+ validate: validatePriorityInteger
2269
+ })
2270
+ );
2271
+ labels[label] = Number(rawValue);
2272
+ }
2273
+ return { priority: { source: "labels", labels }, priorityField: null };
2274
+ }
2275
+ const priorityField = input.priorityResolution.ambiguous.length > 0 ? await promptPriorityField(input.priorityResolution.ambiguous, {
2276
+ stepLabel: "Priority field"
2277
+ }) : input.priorityResolution.field;
2278
+ if (!priorityField) {
2279
+ return { priority: buildDisabledPriority(), priorityField: null };
2280
+ }
2281
+ return {
2282
+ priority: {
2283
+ source: "project-field",
2284
+ field: priorityField.name,
2285
+ values: await promptProjectFieldPriorityValues(priorityField)
2286
+ },
2287
+ priorityField
2288
+ };
2289
+ }
2290
+ function validatePriorityInteger(value) {
2291
+ const trimmed = value.trim();
2292
+ return trimmed !== "" && Number.isInteger(Number(trimmed)) ? void 0 : "Enter an integer.";
2293
+ }
2066
2294
  async function promptStateMappings(statusField, options) {
2067
2295
  const mappings = {};
2068
2296
  const inferred = inferAllStateRoles(statusField.options.map((o) => o.name));
@@ -2092,10 +2320,12 @@ async function promptStateMappings(statusField, options) {
2092
2320
  }
2093
2321
  async function planWorkflowArtifacts(opts) {
2094
2322
  const environment = opts.environment ?? await detectEnvironment(opts.cwd);
2323
+ const priority = opts.priority ?? (opts.priorityField ? buildProjectFieldPriority(opts.priorityField) : buildDisabledPriority());
2095
2324
  const workflowMd = generateWorkflowMarkdown({
2096
2325
  projectId: opts.projectDetail.id,
2097
2326
  stateFieldName: opts.statusField.name,
2098
- priorityFieldName: opts.priorityField?.name ?? null,
2327
+ priority,
2328
+ includePriorityTemplates: opts.includePriorityTemplates ?? priority.source === "disabled",
2099
2329
  mappings: opts.mappings,
2100
2330
  lifecycle: toWorkflowLifecycleConfig(opts.statusField.name, opts.mappings),
2101
2331
  runtime: opts.runtime,
@@ -2112,6 +2342,8 @@ async function planWorkflowArtifacts(opts) {
2112
2342
  projectDetail: opts.projectDetail,
2113
2343
  statusField: opts.statusField,
2114
2344
  priorityField: opts.priorityField,
2345
+ priority,
2346
+ includePriorityTemplates: opts.includePriorityTemplates ?? priority.source === "disabled",
2115
2347
  runtime: opts.runtime,
2116
2348
  skipSkills: opts.skipSkills,
2117
2349
  skipContext: opts.skipContext,
@@ -2146,6 +2378,7 @@ async function planEcosystem(opts) {
2146
2378
  skipSkills,
2147
2379
  skipContext
2148
2380
  } = opts;
2381
+ const priority = opts.priority ?? (priorityField ? buildProjectFieldPriority(priorityField) : buildDisabledPriority());
2149
2382
  const ghSymphonyDir = join3(cwd, ".gh-symphony");
2150
2383
  const environment = opts.environment ?? await detectEnvironment(cwd);
2151
2384
  const files = [];
@@ -2184,7 +2417,7 @@ async function planEcosystem(opts) {
2184
2417
  role: null
2185
2418
  })),
2186
2419
  projectId: projectDetail.id,
2187
- priorityFieldName: priorityField?.name ?? null,
2420
+ priority,
2188
2421
  detectedEnvironment: environment
2189
2422
  });
2190
2423
  files.push(
@@ -2235,7 +2468,7 @@ async function planEcosystem(opts) {
2235
2468
  projectId: projectDetail.id,
2236
2469
  githubProjectTitle: projectDetail.title,
2237
2470
  runtime,
2238
- priorityFieldName: priorityField?.name ?? null,
2471
+ priority,
2239
2472
  skillsDir,
2240
2473
  skipSkills,
2241
2474
  environment,
@@ -2284,7 +2517,7 @@ async function writeEcosystem(opts) {
2284
2517
  projectId: plan.projectId,
2285
2518
  githubProjectTitle: plan.githubProjectTitle,
2286
2519
  runtime: plan.runtime,
2287
- priorityFieldName: plan.priorityFieldName,
2520
+ priority: plan.priority,
2288
2521
  skillsDir: plan.skillsDir,
2289
2522
  skipSkills: plan.skipSkills,
2290
2523
  afterCreateHookWritten,
@@ -2294,6 +2527,23 @@ async function writeEcosystem(opts) {
2294
2527
  skillsSkipped: skillsSkipped.sort()
2295
2528
  };
2296
2529
  }
2530
+ function formatPrioritySummaryLines(priority) {
2531
+ if (priority.source === "disabled") {
2532
+ return ["Priority source disabled"];
2533
+ }
2534
+ if (priority.source === "project-field") {
2535
+ const mapping2 = Object.entries(priority.values).map(([name, value]) => `${name}=${value}`).join(", ");
2536
+ return [
2537
+ "Priority source project-field",
2538
+ `Priority mapping ${priority.field}: ${mapping2 || "none"}`
2539
+ ];
2540
+ }
2541
+ const mapping = Object.entries(priority.labels).map(([name, value]) => `${name}=${value}`).join(", ");
2542
+ return [
2543
+ "Priority source labels",
2544
+ `Priority mapping ${mapping || "none"}`
2545
+ ];
2546
+ }
2297
2547
  function printEcosystemSummary(result, workflowPath, opts) {
2298
2548
  const cwd = process.cwd();
2299
2549
  const relWorkflow = relative(cwd, workflowPath) || "WORKFLOW.md";
@@ -2302,9 +2552,7 @@ function printEcosystemSummary(result, workflowPath, opts) {
2302
2552
  `GitHub Project ${result.githubProjectTitle} (${result.projectId})`
2303
2553
  );
2304
2554
  lines.push(`Runtime ${result.runtime}`);
2305
- if (result.priorityFieldName) {
2306
- lines.push(`Priority field ${result.priorityFieldName}`);
2307
- }
2555
+ lines.push(...formatPrioritySummaryLines(result.priority));
2308
2556
  lines.push("");
2309
2557
  lines.push("Generated files");
2310
2558
  lines.push(` \u2713 WORKFLOW.md ${relWorkflow}`);
@@ -2358,9 +2606,7 @@ function renderDryRunPreview(workflowPath, workflowPlan, ecosystemPlan) {
2358
2606
  `GitHub Project ${ecosystemPlan.githubProjectTitle} (${ecosystemPlan.projectId})`
2359
2607
  );
2360
2608
  lines.push(`Runtime ${ecosystemPlan.runtime}`);
2361
- if (ecosystemPlan.priorityFieldName) {
2362
- lines.push(`Priority field ${ecosystemPlan.priorityFieldName}`);
2363
- }
2609
+ lines.push(...formatPrioritySummaryLines(ecosystemPlan.priority));
2364
2610
  lines.push("");
2365
2611
  lines.push("Planned file changes");
2366
2612
  lines.push(
@@ -2388,7 +2634,7 @@ function buildDryRunJsonResult(workflowPath, workflowPlan, ecosystemPlan) {
2388
2634
  projectId: ecosystemPlan.projectId,
2389
2635
  githubProjectTitle: ecosystemPlan.githubProjectTitle,
2390
2636
  runtime: ecosystemPlan.runtime,
2391
- priorityFieldName: ecosystemPlan.priorityFieldName,
2637
+ priority: ecosystemPlan.priority,
2392
2638
  files: [workflowPlan, ...ecosystemPlan.files].map((file) => ({
2393
2639
  path: file.path,
2394
2640
  label: file.label,
@@ -2476,10 +2722,11 @@ async function runNonInteractive(flags, options) {
2476
2722
  const { field: autoPriorityField, ambiguous: ambiguousPriorityFields } = resolvePriorityField(githubProject, statusField);
2477
2723
  if (ambiguousPriorityFields.length > 0) {
2478
2724
  process.stderr.write(
2479
- `Warning: Multiple priority-like single-select fields found (${ambiguousPriorityFields.map((field) => `"${field.name}"`).join(", ")}). Skipping tracker.priority_field in non-interactive mode.
2725
+ `Warning: Multiple priority-like single-select fields found (${ambiguousPriorityFields.map((field) => `"${field.name}"`).join(", ")}). Writing disabled priority scaffold in non-interactive mode.
2480
2726
  `
2481
2727
  );
2482
2728
  }
2729
+ const priority = autoPriorityField ? buildProjectFieldPriority(autoPriorityField) : buildDisabledPriority();
2483
2730
  const validation = validateStateMapping(mappings);
2484
2731
  if (!validation.valid) {
2485
2732
  process.stderr.write(
@@ -2497,6 +2744,8 @@ Run without --non-interactive for manual mapping.
2497
2744
  projectDetail: githubProject,
2498
2745
  statusField,
2499
2746
  priorityField: autoPriorityField,
2747
+ priority,
2748
+ includePriorityTemplates: !autoPriorityField,
2500
2749
  mappings,
2501
2750
  runtime,
2502
2751
  skipSkills: flags.skipSkills,
@@ -2520,6 +2769,8 @@ Run without --non-interactive for manual mapping.
2520
2769
  projectDetail: githubProject,
2521
2770
  statusField,
2522
2771
  priorityField: autoPriorityField,
2772
+ priority,
2773
+ includePriorityTemplates: !autoPriorityField,
2523
2774
  runtime,
2524
2775
  skipSkills: flags.skipSkills,
2525
2776
  skipContext: flags.skipContext
@@ -2626,15 +2877,18 @@ async function runInteractiveStandalone(flags, _options) {
2626
2877
  return;
2627
2878
  }
2628
2879
  const priorityResolution = resolvePriorityField(projectDetail, statusField);
2880
+ const priorityLabelNames = await collectPriorityLabelNames(
2881
+ client,
2882
+ projectDetail.linkedRepositories
2883
+ );
2629
2884
  const mappings = await promptStateMappings(statusField, {
2630
- stepLabel: priorityResolution.ambiguous.length > 0 ? "Step 3/4" : "Step 3/3"
2885
+ stepLabel: "Step 3/4"
2886
+ });
2887
+ const { priority, priorityField } = await promptPriorityConfig({
2888
+ priorityResolution,
2889
+ labelNames: priorityLabelNames,
2890
+ stepLabel: "Step 4/4"
2631
2891
  });
2632
- let priorityField = priorityResolution.field;
2633
- if (priorityResolution.ambiguous.length > 0) {
2634
- priorityField = await promptPriorityField(priorityResolution.ambiguous, {
2635
- stepLabel: "Step 4/4"
2636
- });
2637
- }
2638
2892
  const validation = validateStateMapping(mappings);
2639
2893
  if (!validation.valid) {
2640
2894
  p.log.error("Mapping validation failed:");
@@ -2654,6 +2908,8 @@ async function runInteractiveStandalone(flags, _options) {
2654
2908
  projectDetail,
2655
2909
  statusField,
2656
2910
  priorityField,
2911
+ priority,
2912
+ includePriorityTemplates: priority.source === "disabled",
2657
2913
  mappings,
2658
2914
  runtime,
2659
2915
  skipSkills: flags.skipSkills,
@@ -2669,6 +2925,8 @@ async function runInteractiveStandalone(flags, _options) {
2669
2925
  projectDetail,
2670
2926
  statusField,
2671
2927
  priorityField,
2928
+ priority,
2929
+ includePriorityTemplates: priority.source === "disabled",
2672
2930
  runtime,
2673
2931
  skipSkills: flags.skipSkills,
2674
2932
  skipContext: flags.skipContext
@@ -2686,6 +2944,8 @@ export {
2686
2944
  resolveStatusField,
2687
2945
  buildAutomaticStateMappings,
2688
2946
  resolvePriorityField,
2947
+ collectPriorityLabelNames,
2948
+ promptPriorityConfig,
2689
2949
  promptStateMappings,
2690
2950
  planWorkflowArtifacts,
2691
2951
  writeWorkflowPlan,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  REPO_RUNTIME_DIR
4
- } from "./chunk-WOVNN5NW.js";
4
+ } from "./chunk-4ICDSQCJ.js";
5
5
 
6
6
  // src/orchestrator-runtime.ts
7
7
  import { resolve } from "path";