@gh-symphony/cli 0.1.4 → 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.
@@ -1,18 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ buildPriorityConfigDiagnostics,
4
+ buildPriorityDriftDiagnostics,
3
5
  parseIssueReference,
4
6
  readGitHubProjectBinding,
5
7
  renderIssueWorkflowPreview
6
- } from "./chunk-HT3FAJAO.js";
7
- import "./chunk-RHLUIMBN.js";
8
+ } from "./chunk-27UZ6KX2.js";
9
+ import "./chunk-PLBG7TZA.js";
8
10
  import {
9
11
  fetchGithubProjectIssueByRepositoryAndNumber,
10
12
  fetchGithubProjectIssues,
11
13
  inspectManagedProjectSelection
12
- } from "./chunk-YIARPBOR.js";
14
+ } from "./chunk-X4QSP3AX.js";
15
+ import "./chunk-FAU72YC2.js";
13
16
  import {
14
17
  resolveRuntimeRoot
15
- } from "./chunk-6I753NYO.js";
18
+ } from "./chunk-RZ3WO7OV.js";
16
19
  import {
17
20
  GitHubApiError,
18
21
  REQUIRED_GH_SCOPES,
@@ -24,24 +27,397 @@ import {
24
27
  getEnvGitHubToken,
25
28
  getGhToken,
26
29
  getProjectDetail,
30
+ listRepositoryLabels,
27
31
  runGhAuthLogin,
28
32
  runGhAuthRefresh,
29
33
  validateGitHubToken
30
- } from "./chunk-Z3NZOPLZ.js";
34
+ } from "./chunk-BOM2BYZQ.js";
31
35
  import {
32
36
  isClaudeRuntimeCommand,
33
37
  parseWorkflowMarkdown,
38
+ redactObservabilityDiagnosticsWithStats,
39
+ redactObservabilityTextWithStats,
34
40
  resolveClaudeCommandBinary,
35
41
  resolveRuntimeCommandBinary,
36
42
  runClaudePreflight
37
- } from "./chunk-EWTMSDCE.js";
38
- import "./chunk-WOVNN5NW.js";
43
+ } from "./chunk-3SKN5L3I.js";
44
+ import {
45
+ configFilePath,
46
+ orchestratorLogPath,
47
+ projectConfigPath
48
+ } from "./chunk-4ICDSQCJ.js";
39
49
 
40
50
  // src/commands/doctor.ts
41
- import { constants } from "fs";
51
+ import { constants as constants2 } from "fs";
42
52
  import { execFileSync, spawnSync } from "child_process";
43
- import { access, mkdir, readFile, stat } from "fs/promises";
44
- import { delimiter, isAbsolute, join, resolve } from "path";
53
+ import { access as access2, mkdir as mkdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
54
+ import { delimiter, isAbsolute, join as join2, resolve as resolve2 } from "path";
55
+
56
+ // src/support/bundle.ts
57
+ import { constants } from "fs";
58
+ import { open } from "fs/promises";
59
+ import {
60
+ access,
61
+ mkdir,
62
+ readdir,
63
+ readFile,
64
+ stat,
65
+ writeFile
66
+ } from "fs/promises";
67
+ import { dirname, join, relative, resolve, sep } from "path";
68
+ var SUPPORT_BUNDLE_LIMITS = {
69
+ maxRuns: 3,
70
+ maxLogBytes: 64 * 1024,
71
+ maxLogLines: 500,
72
+ maxBundleBytes: 5 * 1024 * 1024
73
+ };
74
+ async function createSupportBundle(input) {
75
+ const createdAt = (input.now ?? /* @__PURE__ */ new Date()).toISOString();
76
+ const root = resolveBundleRoot(input.outputPath, input.repoRoot, createdAt);
77
+ await ensureWritableBundleRoot(root);
78
+ const state = {
79
+ root,
80
+ writtenBytes: 0,
81
+ manifest: {
82
+ version: 1,
83
+ createdAt,
84
+ projectId: input.projectId,
85
+ configDir: resolve(input.configDir),
86
+ included: [],
87
+ missing: [],
88
+ redactions: [],
89
+ truncations: [],
90
+ limits: SUPPORT_BUNDLE_LIMITS,
91
+ bundleBytes: {
92
+ written: 0,
93
+ softMax: SUPPORT_BUNDLE_LIMITS.maxBundleBytes,
94
+ exceeded: false
95
+ }
96
+ }
97
+ };
98
+ await writeJsonArtifact(state, "doctor.json", input.doctorReport);
99
+ await copyJsonArtifact(
100
+ state,
101
+ configFilePath(input.configDir),
102
+ "config/config.json"
103
+ );
104
+ await copyJsonArtifact(
105
+ state,
106
+ projectConfigPath(input.configDir, input.projectId),
107
+ "config/project.json"
108
+ );
109
+ await copyTextArtifact(
110
+ state,
111
+ join(input.repoRoot, "WORKFLOW.md"),
112
+ "repo/WORKFLOW.md",
113
+ { bounded: false }
114
+ );
115
+ const runtimeRoot = await resolveRuntimeArtifactRoot(
116
+ input.configDir,
117
+ input.projectId
118
+ );
119
+ await copyJsonArtifact(
120
+ state,
121
+ join(runtimeRoot, "status.json"),
122
+ "runtime/status.json"
123
+ );
124
+ await copyJsonArtifact(
125
+ state,
126
+ join(runtimeRoot, "issues.json"),
127
+ "runtime/issues.json"
128
+ );
129
+ await copyTextArtifact(
130
+ state,
131
+ orchestratorLogPath(input.configDir, input.projectId),
132
+ "runtime/orchestrator.log.tail",
133
+ { bounded: true }
134
+ );
135
+ const recentRuns = await listRecentRuns(input.configDir, input.projectId);
136
+ if (recentRuns.length === 0) {
137
+ state.manifest.missing.push({
138
+ path: "runs",
139
+ reason: "No run records were found for the selected project."
140
+ });
141
+ }
142
+ for (const recentRun of recentRuns) {
143
+ const destinationDir = `runs/${sanitizePathSegment(recentRun.runId)}`;
144
+ if (recentRun.run) {
145
+ await writeJsonArtifact(
146
+ state,
147
+ `${destinationDir}/run.json`,
148
+ recentRun.run
149
+ );
150
+ } else {
151
+ state.manifest.missing.push({
152
+ path: `${destinationDir}/run.json`,
153
+ reason: "Run metadata is missing or unreadable."
154
+ });
155
+ }
156
+ await copyTextArtifact(
157
+ state,
158
+ join(recentRun.runDir, "events.ndjson"),
159
+ `${destinationDir}/events.ndjson.tail`,
160
+ { bounded: true }
161
+ );
162
+ await copyTextArtifact(
163
+ state,
164
+ join(recentRun.runDir, "worker.log"),
165
+ `${destinationDir}/worker.log.tail`,
166
+ { bounded: true }
167
+ );
168
+ }
169
+ await writeManifest(state);
170
+ return buildSummary(state);
171
+ }
172
+ function resolveBundleRoot(outputPath, repoRoot, createdAt) {
173
+ if (outputPath) {
174
+ return resolve(repoRoot, outputPath);
175
+ }
176
+ const timestamp = createdAt.replace(/[:.]/g, "").replace("T", "-");
177
+ return resolve(repoRoot, `gh-symphony-support-bundle-${timestamp}`);
178
+ }
179
+ async function ensureWritableBundleRoot(root) {
180
+ await mkdir(root, { recursive: true });
181
+ const target = await stat(root);
182
+ if (!target.isDirectory()) {
183
+ throw new Error(`Bundle output path is not a directory: ${root}`);
184
+ }
185
+ await access(root, constants.W_OK);
186
+ }
187
+ async function writeJsonArtifact(state, relativePath, value) {
188
+ const redacted = redactObservabilityDiagnosticsWithStats(value);
189
+ addRedactions(state, redacted.redactions);
190
+ await writeBundleFile(
191
+ state,
192
+ relativePath,
193
+ JSON.stringify(redacted.value, null, 2) + "\n"
194
+ );
195
+ }
196
+ async function copyJsonArtifact(state, sourcePath, destinationPath) {
197
+ let raw;
198
+ try {
199
+ raw = await readFile(sourcePath, "utf8");
200
+ } catch (error) {
201
+ recordMissing(state, destinationPath, sourcePath, error);
202
+ return;
203
+ }
204
+ try {
205
+ const parsed = JSON.parse(raw);
206
+ await writeJsonArtifact(state, destinationPath, parsed);
207
+ } catch (error) {
208
+ if (error instanceof SyntaxError) {
209
+ recordMissing(state, destinationPath, sourcePath, error);
210
+ return;
211
+ }
212
+ throw new Error(
213
+ `Failed to redact/write JSON artifact ${sourcePath}: ${formatError(error)}`
214
+ );
215
+ }
216
+ }
217
+ async function copyTextArtifact(state, sourcePath, destinationPath, options) {
218
+ let captured;
219
+ try {
220
+ captured = options.bounded ? await readBoundedTail(sourcePath) : { text: await readFile(sourcePath, "utf8"), truncated: false };
221
+ } catch (error) {
222
+ recordMissing(state, destinationPath, sourcePath, error);
223
+ return;
224
+ }
225
+ const redacted = redactObservabilityTextWithStats(captured.text);
226
+ addRedactions(state, redacted.redactions);
227
+ await writeBundleFile(state, destinationPath, redacted.value);
228
+ if (captured.truncated) {
229
+ state.manifest.truncations.push({
230
+ path: destinationPath,
231
+ originalBytes: captured.originalBytes,
232
+ writtenBytes: Buffer.byteLength(redacted.value, "utf8"),
233
+ maxBytes: SUPPORT_BUNDLE_LIMITS.maxLogBytes,
234
+ maxLines: SUPPORT_BUNDLE_LIMITS.maxLogLines,
235
+ reason: captured.reason ?? "bounded_tail"
236
+ });
237
+ }
238
+ }
239
+ async function readBoundedTail(sourcePath) {
240
+ const handle = await open(sourcePath, "r");
241
+ try {
242
+ const stats = await handle.stat();
243
+ const start = Math.max(0, stats.size - SUPPORT_BUNDLE_LIMITS.maxLogBytes);
244
+ const length = stats.size - start;
245
+ const buffer = Buffer.alloc(length);
246
+ if (length > 0) {
247
+ await handle.read(buffer, 0, length, start);
248
+ }
249
+ let text = buffer.toString("utf8");
250
+ let truncated = start > 0;
251
+ const reasons = [];
252
+ if (start > 0) {
253
+ reasons.push("maxLogBytes");
254
+ const firstNewline = text.indexOf("\n");
255
+ if (firstNewline >= 0) {
256
+ text = text.slice(firstNewline + 1);
257
+ } else {
258
+ reasons.push("partialLine");
259
+ }
260
+ }
261
+ const lines = text.split(/\r?\n/);
262
+ if (lines.length > SUPPORT_BUNDLE_LIMITS.maxLogLines) {
263
+ text = lines.slice(-SUPPORT_BUNDLE_LIMITS.maxLogLines).join("\n");
264
+ truncated = true;
265
+ reasons.push("maxLogLines");
266
+ }
267
+ return {
268
+ text,
269
+ truncated,
270
+ originalBytes: stats.size,
271
+ reason: reasons.join(",") || void 0
272
+ };
273
+ } finally {
274
+ await handle.close();
275
+ }
276
+ }
277
+ async function writeBundleFile(state, relativePath, content) {
278
+ const target = resolveBundlePath(state.root, relativePath);
279
+ await mkdir(dirname(target), { recursive: true });
280
+ await writeFile(target, content, "utf8");
281
+ state.writtenBytes += Buffer.byteLength(content, "utf8");
282
+ state.manifest.included.push(relativePath);
283
+ }
284
+ async function writeManifest(state) {
285
+ if (!state.manifest.included.includes("manifest.json")) {
286
+ state.manifest.included.push("manifest.json");
287
+ }
288
+ state.manifest.bundleBytes = {
289
+ written: state.writtenBytes,
290
+ softMax: SUPPORT_BUNDLE_LIMITS.maxBundleBytes,
291
+ exceeded: state.writtenBytes > SUPPORT_BUNDLE_LIMITS.maxBundleBytes
292
+ };
293
+ const target = resolveBundlePath(state.root, "manifest.json");
294
+ await writeFile(
295
+ target,
296
+ JSON.stringify(state.manifest, null, 2) + "\n",
297
+ "utf8"
298
+ );
299
+ }
300
+ function resolveBundlePath(root, relativePath) {
301
+ const target = resolve(root, relativePath);
302
+ const relativeTarget = relative(root, target);
303
+ if (relativeTarget === "" || relativeTarget.startsWith("..") || relativeTarget.includes(`..${sep}`)) {
304
+ throw new Error(`Refusing to write outside bundle root: ${relativePath}`);
305
+ }
306
+ return target;
307
+ }
308
+ function recordMissing(state, destinationPath, sourcePath, error) {
309
+ state.manifest.missing.push({
310
+ path: destinationPath,
311
+ reason: `${sourcePath}: ${formatError(error)}`
312
+ });
313
+ }
314
+ function addRedactions(state, redactions) {
315
+ const existing = new Map(
316
+ state.manifest.redactions.map((entry) => [entry.class, entry.count])
317
+ );
318
+ for (const redaction of redactions) {
319
+ existing.set(
320
+ redaction.class,
321
+ (existing.get(redaction.class) ?? 0) + redaction.count
322
+ );
323
+ }
324
+ state.manifest.redactions = Array.from(existing.entries()).map(([redactionClass, count]) => ({ class: redactionClass, count })).sort((left, right) => left.class.localeCompare(right.class));
325
+ }
326
+ async function resolveRuntimeArtifactRoot(configDir, projectId) {
327
+ const candidates = [
328
+ resolve(configDir),
329
+ resolve(configDir, "projects", projectId)
330
+ ];
331
+ for (const candidate of candidates) {
332
+ try {
333
+ await access(join(candidate, "status.json"), constants.R_OK);
334
+ return candidate;
335
+ } catch {
336
+ continue;
337
+ }
338
+ }
339
+ return candidates[0];
340
+ }
341
+ async function listRecentRuns(configDir, projectId) {
342
+ const runsDirs = [
343
+ resolve(configDir, "runs"),
344
+ resolve(configDir, "projects", projectId, "runs")
345
+ ];
346
+ const seen = /* @__PURE__ */ new Set();
347
+ const runs = [];
348
+ for (const runsDir of runsDirs) {
349
+ let entries;
350
+ try {
351
+ entries = await readdir(runsDir);
352
+ } catch {
353
+ continue;
354
+ }
355
+ for (const entry of entries) {
356
+ const runId = sanitizePathSegment(entry);
357
+ if (seen.has(runId)) {
358
+ continue;
359
+ }
360
+ const runDir = join(runsDir, entry);
361
+ const runJsonPath = join(runDir, "run.json");
362
+ let run = null;
363
+ let updatedAt = "";
364
+ let active = false;
365
+ try {
366
+ const raw = await readFile(runJsonPath, "utf8");
367
+ run = JSON.parse(raw);
368
+ updatedAt = stringField(run, "updatedAt") ?? stringField(run, "endedAt") ?? stringField(run, "startedAt") ?? "";
369
+ const statusValue = stringField(run, "status");
370
+ active = statusValue === "running" || statusValue === "retrying";
371
+ } catch {
372
+ try {
373
+ const metadata = await stat(runDir);
374
+ updatedAt = metadata.mtime.toISOString();
375
+ } catch {
376
+ updatedAt = "";
377
+ }
378
+ }
379
+ seen.add(runId);
380
+ runs.push({ runId, runDir, run, updatedAt, active });
381
+ }
382
+ }
383
+ return runs.sort((left, right) => {
384
+ if (left.active !== right.active) {
385
+ return left.active ? -1 : 1;
386
+ }
387
+ return right.updatedAt.localeCompare(left.updatedAt);
388
+ }).slice(0, SUPPORT_BUNDLE_LIMITS.maxRuns);
389
+ }
390
+ function stringField(value, key) {
391
+ const field = value[key];
392
+ return typeof field === "string" ? field : null;
393
+ }
394
+ function sanitizePathSegment(value) {
395
+ return value.replace(/[^A-Za-z0-9._-]/g, "_");
396
+ }
397
+ function buildSummary(state) {
398
+ const redactionCount = state.manifest.redactions.reduce(
399
+ (sum, entry) => sum + entry.count,
400
+ 0
401
+ );
402
+ return {
403
+ outputPath: state.root,
404
+ projectId: state.manifest.projectId,
405
+ includedCount: state.manifest.included.length,
406
+ missingCount: state.manifest.missing.length,
407
+ redactionCount,
408
+ redactionClasses: state.manifest.redactions,
409
+ truncationCount: state.manifest.truncations.length,
410
+ manifestPath: join(state.root, "manifest.json")
411
+ };
412
+ }
413
+ function formatError(error) {
414
+ if (error && typeof error === "object" && "code" in error && typeof error.code === "string") {
415
+ return error.code;
416
+ }
417
+ return error instanceof Error ? error.message : String(error);
418
+ }
419
+
420
+ // src/commands/doctor.ts
45
421
  var DEFAULT_DEPENDENCIES = {
46
422
  checkGhInstalled,
47
423
  checkGhAuthenticated,
@@ -52,12 +428,13 @@ var DEFAULT_DEPENDENCIES = {
52
428
  inspectManagedProjectSelection,
53
429
  createClient,
54
430
  getProjectDetail,
431
+ listRepositoryLabels,
55
432
  fetchProjectIssues: fetchGithubProjectIssues,
56
433
  fetchProjectIssue: fetchGithubProjectIssueByRepositoryAndNumber,
57
- readFile,
58
- access,
59
- mkdir,
60
- stat,
434
+ readFile: readFile2,
435
+ access: access2,
436
+ mkdir: mkdir2,
437
+ stat: stat2,
61
438
  parseWorkflowMarkdown,
62
439
  execFileSync,
63
440
  runGhAuthLogin,
@@ -75,9 +452,9 @@ var DEFAULT_DEPENDENCIES = {
75
452
  };
76
453
  var MINIMUM_NODE_MAJOR = 24;
77
454
  var MINIMUM_NODE_VERSION = `v${MINIMUM_NODE_MAJOR}.0.0`;
78
- var DOCTOR_USAGE = "Usage: gh-symphony doctor [--project-id <project-id>] [--fix] [--smoke] [--issue <owner/repo#number>]";
455
+ var DOCTOR_USAGE = "Usage: gh-symphony doctor [--project-id <project-id>] [--fix] [--smoke] [--issue <owner/repo#number>] [--bundle [path]]";
79
456
  function parseDoctorArgs(args) {
80
- const parsed = { fix: false, smoke: false };
457
+ const parsed = { fix: false, smoke: false, bundle: false };
81
458
  for (let i = 0; i < args.length; i += 1) {
82
459
  const arg = args[i];
83
460
  if (arg === "--project" || arg === "--project-id") {
@@ -98,6 +475,15 @@ function parseDoctorArgs(args) {
98
475
  parsed.smoke = true;
99
476
  continue;
100
477
  }
478
+ if (arg === "--bundle") {
479
+ parsed.bundle = true;
480
+ const value = args[i + 1];
481
+ if (value && !value.startsWith("-")) {
482
+ parsed.bundlePath = value;
483
+ i += 1;
484
+ }
485
+ continue;
486
+ }
101
487
  if (arg === "--issue") {
102
488
  const value = args[i + 1];
103
489
  if (!value || value.startsWith("-")) {
@@ -112,10 +498,15 @@ function parseDoctorArgs(args) {
112
498
  parsed.error = `Unknown option '${arg}'`;
113
499
  return parsed;
114
500
  }
501
+ parsed.error = `Unexpected argument '${arg}'`;
502
+ return parsed;
115
503
  }
116
504
  if (parsed.issue && !parsed.smoke) {
117
505
  parsed.error = "Option '--issue' requires '--smoke'";
118
506
  }
507
+ if (parsed.bundle && parsed.fix) {
508
+ parsed.error = "Option '--fix' cannot be used with '--bundle'";
509
+ }
119
510
  return parsed;
120
511
  }
121
512
  function passCheck(id, title, summary, details) {
@@ -160,7 +551,7 @@ async function inspectPathState(targetPath, deps) {
160
551
  };
161
552
  }
162
553
  try {
163
- await deps.access(targetPath, constants.W_OK);
554
+ await deps.access(targetPath, constants2.W_OK);
164
555
  return {
165
556
  exists: true,
166
557
  isDirectory: true,
@@ -254,7 +645,7 @@ async function commandExistsOnPath(binary, deps) {
254
645
  if (isAbsolute(binary) || binary.includes("/") || binary.includes("\\")) {
255
646
  for (const candidate of candidates) {
256
647
  try {
257
- await deps.access(resolve(candidate), constants.X_OK);
648
+ await deps.access(resolve2(candidate), constants2.X_OK);
258
649
  return true;
259
650
  } catch {
260
651
  continue;
@@ -267,9 +658,9 @@ async function commandExistsOnPath(binary, deps) {
267
658
  continue;
268
659
  }
269
660
  for (const command of candidates) {
270
- const candidate = join(segment, command);
661
+ const candidate = join2(segment, command);
271
662
  try {
272
- await deps.access(candidate, constants.X_OK);
663
+ await deps.access(candidate, constants2.X_OK);
273
664
  return true;
274
665
  } catch {
275
666
  continue;
@@ -329,7 +720,7 @@ async function checkGitInstallation(deps) {
329
720
  }
330
721
  }
331
722
  async function checkWorkflow(repoRoot, deps) {
332
- const workflowPath = join(repoRoot, "WORKFLOW.md");
723
+ const workflowPath = join2(repoRoot, "WORKFLOW.md");
333
724
  let markdown;
334
725
  try {
335
726
  markdown = await deps.readFile(workflowPath, "utf8");
@@ -370,10 +761,140 @@ function buildGithubTrackerConfig(input) {
370
761
  apiUrl: input.projectConfig.projectConfig.tracker.apiUrl,
371
762
  lifecycle: input.workflow.lifecycle,
372
763
  assignedOnly: settings?.assignedOnly === true,
764
+ priority: input.workflow.tracker.priority,
373
765
  priorityFieldName: typeof settings?.priorityFieldName === "string" ? settings.priorityFieldName : void 0,
374
766
  timeoutMs: typeof settings?.timeoutMs === "number" ? settings.timeoutMs : void 0
375
767
  };
376
768
  }
769
+ async function buildPriorityMappingChecks(input) {
770
+ if (input.workflow.status !== "pass") {
771
+ return [];
772
+ }
773
+ const configDiagnostics = buildPriorityConfigDiagnostics(
774
+ input.workflow.workflow
775
+ );
776
+ const checks = configDiagnostics.map(
777
+ (diagnostic) => warnCheck(
778
+ "priority_mapping",
779
+ diagnostic.title,
780
+ diagnostic.summary,
781
+ diagnostic.remediation,
782
+ diagnostic.details
783
+ )
784
+ );
785
+ const priority = input.workflow.workflow.tracker.priority;
786
+ const parsedWorkflow = input.workflow.workflow;
787
+ if (input.workflow.workflow.tracker.kind !== "github-project" || !priority || priority.source === "disabled") {
788
+ if (checks.length === 0) {
789
+ checks.push(
790
+ passCheck(
791
+ "priority_mapping",
792
+ "Priority mapping",
793
+ priority?.source === "disabled" ? "Explicit priority mapping is disabled; dispatch priority resolves to null." : "No explicit priority mapping drift checks are required.",
794
+ { source: priority?.source ?? null }
795
+ )
796
+ );
797
+ }
798
+ return checks;
799
+ }
800
+ if (!input.auth || !input.selection || input.selection.kind !== "resolved" || !input.projectDetail || !input.projectBindingId) {
801
+ checks.push(
802
+ warnCheck(
803
+ "priority_mapping",
804
+ "Priority mapping drift",
805
+ "Live priority mapping drift checks could not run because GitHub authentication, managed project selection, or project resolution is unavailable.",
806
+ "Fix the prerequisite doctor checks, then re-run 'gh-symphony doctor'.",
807
+ {
808
+ blockedBy: [
809
+ ...!input.auth ? ["gh_authentication"] : [],
810
+ ...!input.selection || input.selection.kind !== "resolved" ? ["managed_project"] : [],
811
+ ...!input.projectDetail || !input.projectBindingId ? ["github_project_resolution"] : []
812
+ ]
813
+ }
814
+ )
815
+ );
816
+ return checks;
817
+ }
818
+ const client = input.deps.createClient(input.auth.token, {
819
+ apiUrl: input.selection.projectConfig.tracker.apiUrl
820
+ });
821
+ let repositoryLabels = priority.source === "labels" ? [] : null;
822
+ if (priority.source === "labels") {
823
+ try {
824
+ repositoryLabels = await Promise.all(
825
+ input.projectDetail.linkedRepositories.map(async (repository) => ({
826
+ repository: `${repository.owner}/${repository.name}`,
827
+ labels: (await input.deps.listRepositoryLabels(
828
+ client,
829
+ repository.owner,
830
+ repository.name
831
+ )).map((label) => label.name)
832
+ }))
833
+ );
834
+ } catch (error) {
835
+ checks.push(
836
+ warnCheck(
837
+ "priority_mapping",
838
+ "Priority label drift",
839
+ "Live repository labels could not be read for priority mapping drift checks.",
840
+ "Confirm GitHub token repository access and re-run 'gh-symphony doctor'.",
841
+ { error: formatSmokeError(error) }
842
+ )
843
+ );
844
+ repositoryLabels = null;
845
+ }
846
+ }
847
+ let activeIssues = [];
848
+ try {
849
+ const trackerConfig = buildGithubTrackerConfig({
850
+ projectConfig: input.selection,
851
+ bindingId: input.projectBindingId,
852
+ token: input.auth.token,
853
+ workflow: input.workflow.workflow
854
+ });
855
+ activeIssues = (await input.deps.fetchProjectIssues(trackerConfig)).filter(
856
+ (issue) => isActiveSmokeIssue(issue, parsedWorkflow)
857
+ );
858
+ } catch (error) {
859
+ checks.push(
860
+ warnCheck(
861
+ "priority_mapping",
862
+ "Active priority drift",
863
+ "Active issues could not be read for priority mapping drift checks.",
864
+ "Confirm GitHub token scopes, project visibility, and network access, then re-run 'gh-symphony doctor'.",
865
+ { error: formatSmokeError(error) }
866
+ )
867
+ );
868
+ }
869
+ const driftDiagnostics = buildPriorityDriftDiagnostics({
870
+ workflow: parsedWorkflow,
871
+ projectDetail: input.projectDetail,
872
+ repositoryLabels,
873
+ activeIssues
874
+ });
875
+ checks.push(
876
+ ...driftDiagnostics.map(
877
+ (diagnostic) => warnCheck(
878
+ "priority_mapping",
879
+ diagnostic.title,
880
+ diagnostic.summary,
881
+ diagnostic.remediation,
882
+ diagnostic.details
883
+ )
884
+ )
885
+ );
886
+ if (checks.length === 0) {
887
+ checks.push(
888
+ passCheck(
889
+ "priority_mapping",
890
+ "Priority mapping",
891
+ "Explicit priority mapping matches the live Project/repository state inspected by doctor.",
892
+ { source: priority.source }
893
+ )
894
+ );
895
+ }
896
+ return checks;
897
+ }
377
898
  function isActiveSmokeIssue(issue, workflow) {
378
899
  const normalized = issue.state.trim().toLowerCase();
379
900
  return workflow.lifecycle.activeStates.some(
@@ -431,9 +952,9 @@ async function buildHookChecks(repoRoot, workflow, deps) {
431
952
  inline += 1;
432
953
  continue;
433
954
  }
434
- const path = isAbsolute(command) ? command : resolve(repoRoot, command);
955
+ const path = isAbsolute(command) ? command : resolve2(repoRoot, command);
435
956
  try {
436
- await deps.access(path, constants.F_OK);
957
+ await deps.access(path, constants2.F_OK);
437
958
  checked.push({ hook, command, path });
438
959
  } catch {
439
960
  unresolved.push({ hook, command, path });
@@ -446,7 +967,13 @@ async function buildHookChecks(repoRoot, workflow, deps) {
446
967
  "Workflow hook paths",
447
968
  `Unresolved WORKFLOW.md hook path${unresolved.length === 1 ? "" : "s"}: ${unresolved.map((entry) => `${entry.hook}=${entry.command}`).join(", ")}.`,
448
969
  "Create the referenced hook script(s), fix the hook path(s), or replace them with inline commands.",
449
- { configured: configured.length, pathsChecked: checked.length, inline, unresolved, checked }
970
+ {
971
+ configured: configured.length,
972
+ pathsChecked: checked.length,
973
+ inline,
974
+ unresolved,
975
+ checked
976
+ }
450
977
  )
451
978
  ];
452
979
  }
@@ -457,7 +984,12 @@ async function buildHookChecks(repoRoot, workflow, deps) {
457
984
  "workflow_hooks",
458
985
  "Workflow hook paths",
459
986
  `${pathSummary}${inlineSummary}`,
460
- { configured: configured.length, pathsChecked: checked.length, inline, checked }
987
+ {
988
+ configured: configured.length,
989
+ pathsChecked: checked.length,
990
+ inline,
991
+ checked
992
+ }
461
993
  )
462
994
  ];
463
995
  }
@@ -1144,6 +1676,16 @@ ${DOCTOR_USAGE}`);
1144
1676
  )
1145
1677
  );
1146
1678
  }
1679
+ checks.push(
1680
+ ...await buildPriorityMappingChecks({
1681
+ auth,
1682
+ selection: resolvedProjectConfig,
1683
+ workflow,
1684
+ projectDetail: resolvedGithubProjectDetail,
1685
+ projectBindingId: resolvedGithubProjectBindingId,
1686
+ deps
1687
+ })
1688
+ );
1147
1689
  if (parsedArgs.smoke) {
1148
1690
  checks.push(
1149
1691
  ...await buildDoctorSmokeChecks({
@@ -1267,7 +1809,7 @@ async function ensureDirectoryRemediation(check, deps) {
1267
1809
  }
1268
1810
  try {
1269
1811
  await deps.mkdir(pathValue, { recursive: true });
1270
- await deps.access(pathValue, constants.W_OK);
1812
+ await deps.access(pathValue, constants2.W_OK);
1271
1813
  const target = await deps.stat(pathValue);
1272
1814
  if (!target.isDirectory()) {
1273
1815
  return remediationStep(
@@ -1563,6 +2105,19 @@ function renderTextReport(report) {
1563
2105
  );
1564
2106
  return lines.join("\n");
1565
2107
  }
2108
+ function renderBundleSummary(summary) {
2109
+ const redactionClasses = summary.redactionClasses.length === 0 ? "none" : summary.redactionClasses.map((entry) => `${entry.class}:${entry.count}`).join(", ");
2110
+ return [
2111
+ "gh-symphony doctor support bundle",
2112
+ `Output path: ${summary.outputPath}`,
2113
+ `Project: ${summary.projectId}`,
2114
+ `Included artifacts: ${summary.includedCount}`,
2115
+ `Missing artifacts: ${summary.missingCount}`,
2116
+ `Redactions: ${summary.redactionCount} (${redactionClasses})`,
2117
+ `Truncations: ${summary.truncationCount}`,
2118
+ `Manifest: ${summary.manifestPath}`
2119
+ ].join("\n");
2120
+ }
1566
2121
  async function runDoctorCommand(args, options, dependencies = {}) {
1567
2122
  try {
1568
2123
  const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
@@ -1572,6 +2127,27 @@ async function runDoctorCommand(args, options, dependencies = {}) {
1572
2127
  ${DOCTOR_USAGE}`);
1573
2128
  }
1574
2129
  const initialReport = await runDoctorDiagnostics(options, args, deps);
2130
+ if (parsedArgs.bundle) {
2131
+ if (!initialReport.projectId) {
2132
+ throw new Error(
2133
+ "Cannot create a support bundle because no managed project was resolved."
2134
+ );
2135
+ }
2136
+ const summary = await createSupportBundle({
2137
+ configDir: options.configDir,
2138
+ projectId: initialReport.projectId,
2139
+ repoRoot: process.cwd(),
2140
+ outputPath: parsedArgs.bundlePath,
2141
+ doctorReport: initialReport
2142
+ });
2143
+ if (options.json) {
2144
+ process.stdout.write(JSON.stringify(summary, null, 2) + "\n");
2145
+ } else {
2146
+ process.stdout.write(renderBundleSummary(summary) + "\n");
2147
+ }
2148
+ process.exitCode = initialReport.ok ? 0 : 1;
2149
+ return;
2150
+ }
1575
2151
  if (parsedArgs.fix) {
1576
2152
  const remediation = {
1577
2153
  attempted: true,