@gh-symphony/cli 0.2.0 → 0.2.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.
package/README.md CHANGED
@@ -37,6 +37,7 @@ gh-symphony doctor
37
37
  gh-symphony doctor --fix
38
38
  gh-symphony doctor --json
39
39
  gh-symphony doctor --smoke
40
+ gh-symphony doctor --bundle
40
41
  GITHUB_GRAPHQL_TOKEN=ghp_your_classic_token gh-symphony doctor --json
41
42
  ```
42
43
 
@@ -107,6 +108,56 @@ You can further customize the agent's behavior by editing `WORKFLOW.md` — this
107
108
 
108
109
  > Currently supported runtimes: **Codex**, **Claude Code**
109
110
 
111
+ ### Explicit Priority Mapping
112
+
113
+ GitHub Project V2 does not have a native issue priority. For GitHub Project workflows, dispatch priority is controlled only by the explicit `tracker.priority` policy in `WORKFLOW.md`; there is no fallback from Project fields to labels and no guessed label naming convention. Unmapped values resolve to `priority = null`, so dispatch falls back to created time and identifier.
114
+
115
+ Project field source:
116
+
117
+ ```yaml
118
+ tracker:
119
+ kind: github-project
120
+ project_id: PVT_kwDOxxxxxx
121
+ state_field: Status
122
+ priority:
123
+ source: project-field
124
+ field: Priority
125
+ values:
126
+ Urgent: 0
127
+ High: 1
128
+ Medium: 2
129
+ Low: 3
130
+ ```
131
+
132
+ Label source:
133
+
134
+ ```yaml
135
+ tracker:
136
+ kind: github-project
137
+ project_id: PVT_kwDOxxxxxx
138
+ state_field: Status
139
+ priority:
140
+ source: labels
141
+ labels:
142
+ P0: 0
143
+ P1: 1
144
+ P2: 2
145
+ P3: 3
146
+ ```
147
+
148
+ Disabled:
149
+
150
+ ```yaml
151
+ tracker:
152
+ kind: github-project
153
+ priority:
154
+ source: disabled
155
+ ```
156
+
157
+ Legacy `tracker.priority_field: Priority` still works, but it is deprecated because it derives numeric priority from the live Project option order. Migrate by copying the field name into `tracker.priority.field` and writing each option display name under `values` with the intended number. If both keys are present, `tracker.priority` wins and `gh-symphony doctor` reports a warning.
158
+
159
+ Run `gh-symphony workflow validate` for local schema errors and `gh-symphony doctor` for live drift warnings such as missing Project fields, missing labels, unmapped live options, stale mappings, and active issues whose priority-like value resolves to `priority = null`.
160
+
110
161
  ### Linear Tracker Repositories
111
162
 
112
163
  For Linear, configure the tracker in `WORKFLOW.md` and initialize the repository runtime from the target GitHub repository:
@@ -303,6 +354,22 @@ Without `--issue`, doctor auto-selects one active live issue from the managed pr
303
354
  - launches `gh-symphony setup` when repository runtime setup or GitHub Project binding must be repaired
304
355
  - prints concrete runtime install guidance when the configured command is missing on `PATH`
305
356
 
357
+ `gh-symphony doctor --bundle` creates a redacted support bundle for bug reports:
358
+
359
+ ```bash
360
+ gh-symphony doctor --bundle
361
+ gh-symphony doctor --bundle ./tmp/support-bundle
362
+ gh-symphony doctor --bundle --project-id your-project-id
363
+ gh-symphony doctor --bundle --json
364
+ ```
365
+
366
+ The bundle writes a deterministic directory containing `manifest.json`,
367
+ `doctor.json`, redacted CLI/project config, `WORKFLOW.md`, runtime
368
+ `status.json`/`issues.json` when available, and bounded recent run
369
+ `events.ndjson`, `worker.log`, and `orchestrator.log` tails. Missing optional
370
+ artifacts are listed in `manifest.missing`; redaction and truncation counts are
371
+ reported in the command summary.
372
+
306
373
  The diagnostic checks cover:
307
374
 
308
375
  - the active GitHub auth source (`GITHUB_GRAPHQL_TOKEN` first, otherwise `gh`) and required scopes
@@ -321,6 +388,7 @@ Use JSON output for scripts and CI smoke checks. `--fix --json` includes a remed
321
388
  gh-symphony doctor --json
322
389
  gh-symphony doctor --fix --json
323
390
  gh-symphony doctor --smoke --json
391
+ gh-symphony doctor --bundle --json
324
392
  gh-symphony repo start --once
325
393
  ```
326
394
 
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  workflow_init_default
4
- } from "./chunk-F46FTZJE.js";
4
+ } from "./chunk-PLBG7TZA.js";
5
5
  import {
6
6
  fetchGithubProjectIssueByRepositoryAndNumber,
7
7
  inspectManagedProjectSelection,
8
8
  resolveTrackerAdapter
9
- } from "./chunk-CTTFIZYG.js";
9
+ } from "./chunk-X4QSP3AX.js";
10
10
  import {
11
11
  GitHubApiError,
12
12
  createClient,
@@ -14,19 +14,198 @@ import {
14
14
  getGhTokenWithSource,
15
15
  getProjectDetail,
16
16
  validateGitHubToken
17
- } from "./chunk-Z3NZOPLZ.js";
17
+ } from "./chunk-BOM2BYZQ.js";
18
18
  import {
19
19
  buildPromptVariables,
20
20
  parseWorkflowMarkdown,
21
21
  renderPrompt
22
- } from "./chunk-Q3UEPUE3.js";
22
+ } from "./chunk-3SKN5L3I.js";
23
23
  import {
24
24
  loadActiveProjectConfig
25
- } from "./chunk-WOVNN5NW.js";
25
+ } from "./chunk-4ICDSQCJ.js";
26
26
 
27
27
  // src/commands/workflow.ts
28
28
  import { readFile } from "fs/promises";
29
29
  import { resolve } from "path";
30
+
31
+ // src/priority-diagnostics.ts
32
+ function buildPriorityConfigDiagnostics(workflow) {
33
+ const diagnostics = [];
34
+ if (workflow.tracker.priorityFieldName) {
35
+ if (workflow.tracker.priority) {
36
+ diagnostics.push({
37
+ title: "Priority mapping precedence",
38
+ summary: "Both legacy tracker.priority_field and explicit tracker.priority are configured; explicit tracker.priority wins.",
39
+ remediation: "Remove tracker.priority_field after confirming the explicit tracker.priority mapping is correct.",
40
+ details: {
41
+ priorityFieldName: workflow.tracker.priorityFieldName,
42
+ explicitSource: workflow.tracker.priority.source
43
+ }
44
+ });
45
+ } else {
46
+ diagnostics.push({
47
+ title: "Legacy priority mapping",
48
+ summary: "tracker.priority_field is deprecated and still supported with legacy Project option-order semantics.",
49
+ remediation: "Migrate to tracker.priority with an explicit project-field, labels, or disabled source.",
50
+ details: {
51
+ priorityFieldName: workflow.tracker.priorityFieldName
52
+ }
53
+ });
54
+ }
55
+ }
56
+ return diagnostics;
57
+ }
58
+ function buildPriorityDriftDiagnostics(input) {
59
+ const priority = input.workflow.tracker.priority;
60
+ if (!priority || priority.source === "disabled") {
61
+ return [];
62
+ }
63
+ if (priority.source === "project-field") {
64
+ return buildProjectFieldDriftDiagnostics({
65
+ priority,
66
+ projectDetail: input.projectDetail,
67
+ activeIssues: input.activeIssues
68
+ });
69
+ }
70
+ return buildLabelDriftDiagnostics({
71
+ priority,
72
+ repositoryLabels: input.repositoryLabels,
73
+ activeIssues: input.activeIssues
74
+ });
75
+ }
76
+ function buildProjectFieldDriftDiagnostics(input) {
77
+ const diagnostics = [];
78
+ const field = input.projectDetail.statusFields.find(
79
+ (candidate) => candidate.name === input.priority.field
80
+ );
81
+ if (!field) {
82
+ diagnostics.push({
83
+ title: "Priority Project field drift",
84
+ summary: `Configured priority field "${input.priority.field}" was not found in the GitHub Project schema.`,
85
+ remediation: "Update tracker.priority.field to the exact live Project V2 single-select field name, or disable priority mapping.",
86
+ details: {
87
+ field: input.priority.field,
88
+ availableFields: input.projectDetail.statusFields.map(
89
+ (candidate) => candidate.name
90
+ )
91
+ }
92
+ });
93
+ return diagnostics;
94
+ }
95
+ const liveOptions = new Set(field.options.map((option) => option.name));
96
+ const mappedOptions = new Set(Object.keys(input.priority.values));
97
+ const unmappedLiveOptions = [...liveOptions].filter(
98
+ (option) => !mappedOptions.has(option)
99
+ );
100
+ const missingConfiguredOptions = [...mappedOptions].filter(
101
+ (option) => !liveOptions.has(option)
102
+ );
103
+ if (unmappedLiveOptions.length > 0) {
104
+ diagnostics.push({
105
+ title: "Unmapped priority Project options",
106
+ summary: `Priority field "${field.name}" has live option(s) not mapped in tracker.priority.values: ${unmappedLiveOptions.join(", ")}.`,
107
+ remediation: "Add explicit numeric mappings for these options or accept that issues holding them resolve to priority = null.",
108
+ details: { field: field.name, unmappedLiveOptions }
109
+ });
110
+ }
111
+ if (missingConfiguredOptions.length > 0) {
112
+ diagnostics.push({
113
+ title: "Missing priority Project options",
114
+ summary: `tracker.priority.values references option(s) that do not exist in priority field "${field.name}": ${missingConfiguredOptions.join(", ")}.`,
115
+ remediation: "Rename the mapping keys to match live Project option display names or remove stale mappings.",
116
+ details: { field: field.name, missingConfiguredOptions }
117
+ });
118
+ }
119
+ const activeUnmapped = input.activeIssues.flatMap((issue) => {
120
+ const rawValue = issue.metadata?.[field.name];
121
+ return typeof rawValue === "string" && rawValue.length > 0 && !mappedOptions.has(rawValue) ? [{ issue: issue.identifier, value: rawValue }] : [];
122
+ });
123
+ if (activeUnmapped.length > 0) {
124
+ diagnostics.push({
125
+ title: "Active issues with unmapped priority values",
126
+ summary: `Active issue(s) currently hold unmapped "${field.name}" value(s); they resolve to priority = null.`,
127
+ remediation: "Map the live value in tracker.priority.values, change the issue value, or leave it unmapped intentionally.",
128
+ details: { field: field.name, activeUnmapped }
129
+ });
130
+ }
131
+ return diagnostics;
132
+ }
133
+ function buildLabelDriftDiagnostics(input) {
134
+ const diagnostics = [];
135
+ const configuredLabels = Object.keys(input.priority.labels);
136
+ if (input.repositoryLabels) {
137
+ const missingByRepository = input.repositoryLabels.flatMap((snapshot) => {
138
+ const live = new Set(snapshot.labels);
139
+ const missing = configuredLabels.filter((label) => !live.has(label));
140
+ return missing.length > 0 ? [{ repository: snapshot.repository, missing }] : [];
141
+ });
142
+ if (missingByRepository.length > 0) {
143
+ diagnostics.push({
144
+ title: "Missing configured priority labels",
145
+ summary: "One or more configured tracker.priority.labels entries are absent from linked repositories.",
146
+ remediation: "Create the labels in each linked repository, rename the mapping keys to exact live labels, or remove stale mappings.",
147
+ details: { missingByRepository }
148
+ });
149
+ }
150
+ const liveLabels = new Set(
151
+ input.repositoryLabels.flatMap((snapshot) => snapshot.labels)
152
+ );
153
+ const missingEverywhere = configuredLabels.filter(
154
+ (label) => !liveLabels.has(label)
155
+ );
156
+ if (missingEverywhere.length > 0) {
157
+ diagnostics.push({
158
+ title: "Stale priority label mappings",
159
+ summary: `tracker.priority.labels references label(s) that do not exist in any linked repository: ${missingEverywhere.join(", ")}.`,
160
+ remediation: "Rename the mapping keys to exact live labels, create those labels, or remove stale mappings.",
161
+ details: { missingEverywhere }
162
+ });
163
+ }
164
+ }
165
+ const configuredLabelByNormalized = new Map(
166
+ configuredLabels.map((label) => [normalizeLabelForComparison(label), label])
167
+ );
168
+ const activeConflicts = input.activeIssues.flatMap((issue) => {
169
+ const matches = issue.labels.flatMap((label) => {
170
+ const configuredLabel = configuredLabelByNormalized.get(
171
+ normalizeLabelForComparison(label)
172
+ );
173
+ return configuredLabel ? [configuredLabel] : [];
174
+ });
175
+ return matches.length > 1 ? [{ issue: issue.identifier, labels: matches }] : [];
176
+ });
177
+ if (activeConflicts.length > 0) {
178
+ diagnostics.push({
179
+ title: "Active issues with multiple priority labels",
180
+ summary: "Active issue(s) have multiple configured priority labels; runtime chooses the lowest numeric value and emits priority.label_conflict_resolved.",
181
+ remediation: "Remove extra priority labels from the issue if only one priority label should apply.",
182
+ details: { activeConflicts }
183
+ });
184
+ }
185
+ const activeUnmapped = input.activeIssues.flatMap((issue) => {
186
+ const labels = issue.labels.filter(
187
+ (label) => isPriorityLikeLabel(label) && !configuredLabelByNormalized.has(normalizeLabelForComparison(label))
188
+ );
189
+ return labels.length > 0 ? [{ issue: issue.identifier, labels }] : [];
190
+ });
191
+ if (activeUnmapped.length > 0) {
192
+ diagnostics.push({
193
+ title: "Active issues with unmapped priority labels",
194
+ summary: "Active issue(s) have priority-like labels not mapped by tracker.priority.labels; those labels do not affect dispatch priority.",
195
+ remediation: "Add explicit mappings for these labels, rename the issue labels, or leave them unmapped intentionally.",
196
+ details: { activeUnmapped }
197
+ });
198
+ }
199
+ return diagnostics;
200
+ }
201
+ function isPriorityLikeLabel(label) {
202
+ return /^(p\d+|priority[:/\s_-].+|prio[:/\s_-].+)$/i.test(label.trim());
203
+ }
204
+ function normalizeLabelForComparison(label) {
205
+ return label.trim().toLowerCase();
206
+ }
207
+
208
+ // src/commands/workflow.ts
30
209
  var SAMPLE_ISSUE = {
31
210
  id: "issue-157-sample",
32
211
  identifier: "octo/hello-world#157",
@@ -416,7 +595,7 @@ function renderIssueWorkflowPreview(input) {
416
595
  function formatAuthError(error) {
417
596
  return `GitHub authentication is required for live issue preview. ${error.message}`;
418
597
  }
419
- async function loadLiveIssue(issueReference, projectId, options) {
598
+ async function loadLiveIssue(issueReference, projectId, workflow, options) {
420
599
  const issue = parseIssueReference(issueReference);
421
600
  const selection = await workflowCommandDependencies.resolveManagedProjectSelection({
422
601
  configDir: options.configDir,
@@ -470,6 +649,7 @@ async function loadLiveIssue(issueReference, projectId, options) {
470
649
  token: auth.token,
471
650
  apiUrl: selection.projectConfig.tracker.apiUrl,
472
651
  assignedOnly: selection.projectConfig.tracker.settings?.assignedOnly === true,
652
+ priority: workflow.tracker.priority,
473
653
  priorityFieldName: typeof selection.projectConfig.tracker.settings?.priorityFieldName === "string" ? selection.projectConfig.tracker.settings.priorityFieldName : void 0,
474
654
  timeoutMs: typeof selection.projectConfig.tracker.settings?.timeoutMs === "number" ? selection.projectConfig.tracker.settings.timeoutMs : void 0
475
655
  },
@@ -564,6 +744,7 @@ function validateWorkflow(workflowPath, markdown) {
564
744
  promptRetry: "pass",
565
745
  continuationGuidance: continuationGuidanceStatus
566
746
  },
747
+ warnings: buildPriorityConfigDiagnostics(workflow),
567
748
  summary: {
568
749
  trackerKind: workflow.tracker.kind,
569
750
  githubProjectId: workflow.githubProjectId,
@@ -634,6 +815,17 @@ Hooks
634
815
  before_remove=${report.summary.hooks.beforeRemove ?? "unset"}
635
816
  hooks.timeout_ms=${report.summary.hooks.timeoutMs}
636
817
  `);
818
+ if (report.warnings.length > 0) {
819
+ process.stdout.write("\nWarnings\n");
820
+ for (const warning of report.warnings) {
821
+ process.stdout.write(` ${warning.title}: ${warning.summary}
822
+ `);
823
+ if (warning.remediation) {
824
+ process.stdout.write(` Fix: ${warning.remediation}
825
+ `);
826
+ }
827
+ }
828
+ }
637
829
  }
638
830
  async function runValidate(args, options) {
639
831
  const flags = parseValidateFlags(args);
@@ -660,7 +852,7 @@ async function runPreview(args, options) {
660
852
  "Live issue preview requires 'tracker.kind: github-project' with owner/repo#number or 'tracker.kind: linear' with a Linear identifier such as ENG-123."
661
853
  );
662
854
  }
663
- const { issue, sampleSource } = flags.issue ? workflow.tracker.kind === "linear" ? await loadLinearIssue(flags.issue, workflow, options) : await loadLiveIssue(flags.issue, flags.projectId, options) : await loadSampleIssue(flags.sample);
855
+ const { issue, sampleSource } = flags.issue ? workflow.tracker.kind === "linear" ? await loadLinearIssue(flags.issue, workflow, options) : await loadLiveIssue(flags.issue, flags.projectId, workflow, options) : await loadSampleIssue(flags.sample);
664
856
  const renderedPrompt = renderIssueWorkflowPreview({
665
857
  workflow,
666
858
  issue,
@@ -731,6 +923,8 @@ var handler = async (args, options) => {
731
923
  var workflow_default = handler;
732
924
 
733
925
  export {
926
+ buildPriorityConfigDiagnostics,
927
+ buildPriorityDriftDiagnostics,
734
928
  setWorkflowCommandDependenciesForTest,
735
929
  resetWorkflowCommandDependenciesForTest,
736
930
  parseIssueReference,