@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.
@@ -1,19 +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-JU3WSGMZ.js";
7
- import "./chunk-F46FTZJE.js";
8
+ } from "./chunk-AA3T5AAJ.js";
9
+ import "./chunk-PLBG7TZA.js";
8
10
  import {
9
11
  fetchGithubProjectIssueByRepositoryAndNumber,
10
12
  fetchGithubProjectIssues,
11
13
  inspectManagedProjectSelection
12
- } from "./chunk-CTTFIZYG.js";
13
- import "./chunk-B6OHDUSH.js";
14
+ } from "./chunk-ZGNAAHLD.js";
15
+ import "./chunk-FAU72YC2.js";
14
16
  import {
15
17
  resolveRuntimeRoot
16
- } from "./chunk-6I753NYO.js";
18
+ } from "./chunk-RZ3WO7OV.js";
17
19
  import {
18
20
  GitHubApiError,
19
21
  REQUIRED_GH_SCOPES,
@@ -25,24 +27,397 @@ import {
25
27
  getEnvGitHubToken,
26
28
  getGhToken,
27
29
  getProjectDetail,
30
+ listRepositoryLabels,
28
31
  runGhAuthLogin,
29
32
  runGhAuthRefresh,
30
33
  validateGitHubToken
31
- } from "./chunk-Z3NZOPLZ.js";
34
+ } from "./chunk-BOM2BYZQ.js";
32
35
  import {
33
36
  isClaudeRuntimeCommand,
34
37
  parseWorkflowMarkdown,
38
+ redactObservabilityDiagnosticsWithStats,
39
+ redactObservabilityTextWithStats,
35
40
  resolveClaudeCommandBinary,
36
41
  resolveRuntimeCommandBinary,
37
42
  runClaudePreflight
38
- } from "./chunk-Q3UEPUE3.js";
39
- import "./chunk-WOVNN5NW.js";
43
+ } from "./chunk-3SKN5L3I.js";
44
+ import {
45
+ configFilePath,
46
+ orchestratorLogPath,
47
+ projectConfigPath
48
+ } from "./chunk-4ICDSQCJ.js";
40
49
 
41
50
  // src/commands/doctor.ts
42
- import { constants } from "fs";
51
+ import { constants as constants2 } from "fs";
43
52
  import { execFileSync, spawnSync } from "child_process";
44
- import { access, mkdir, readFile, stat } from "fs/promises";
45
- 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
46
421
  var DEFAULT_DEPENDENCIES = {
47
422
  checkGhInstalled,
48
423
  checkGhAuthenticated,
@@ -53,12 +428,13 @@ var DEFAULT_DEPENDENCIES = {
53
428
  inspectManagedProjectSelection,
54
429
  createClient,
55
430
  getProjectDetail,
431
+ listRepositoryLabels,
56
432
  fetchProjectIssues: fetchGithubProjectIssues,
57
433
  fetchProjectIssue: fetchGithubProjectIssueByRepositoryAndNumber,
58
- readFile,
59
- access,
60
- mkdir,
61
- stat,
434
+ readFile: readFile2,
435
+ access: access2,
436
+ mkdir: mkdir2,
437
+ stat: stat2,
62
438
  parseWorkflowMarkdown,
63
439
  execFileSync,
64
440
  runGhAuthLogin,
@@ -76,9 +452,9 @@ var DEFAULT_DEPENDENCIES = {
76
452
  };
77
453
  var MINIMUM_NODE_MAJOR = 24;
78
454
  var MINIMUM_NODE_VERSION = `v${MINIMUM_NODE_MAJOR}.0.0`;
79
- 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]]";
80
456
  function parseDoctorArgs(args) {
81
- const parsed = { fix: false, smoke: false };
457
+ const parsed = { fix: false, smoke: false, bundle: false };
82
458
  for (let i = 0; i < args.length; i += 1) {
83
459
  const arg = args[i];
84
460
  if (arg === "--project" || arg === "--project-id") {
@@ -99,6 +475,15 @@ function parseDoctorArgs(args) {
99
475
  parsed.smoke = true;
100
476
  continue;
101
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
+ }
102
487
  if (arg === "--issue") {
103
488
  const value = args[i + 1];
104
489
  if (!value || value.startsWith("-")) {
@@ -113,10 +498,15 @@ function parseDoctorArgs(args) {
113
498
  parsed.error = `Unknown option '${arg}'`;
114
499
  return parsed;
115
500
  }
501
+ parsed.error = `Unexpected argument '${arg}'`;
502
+ return parsed;
116
503
  }
117
504
  if (parsed.issue && !parsed.smoke) {
118
505
  parsed.error = "Option '--issue' requires '--smoke'";
119
506
  }
507
+ if (parsed.bundle && parsed.fix) {
508
+ parsed.error = "Option '--fix' cannot be used with '--bundle'";
509
+ }
120
510
  return parsed;
121
511
  }
122
512
  function passCheck(id, title, summary, details) {
@@ -161,7 +551,7 @@ async function inspectPathState(targetPath, deps) {
161
551
  };
162
552
  }
163
553
  try {
164
- await deps.access(targetPath, constants.W_OK);
554
+ await deps.access(targetPath, constants2.W_OK);
165
555
  return {
166
556
  exists: true,
167
557
  isDirectory: true,
@@ -255,7 +645,7 @@ async function commandExistsOnPath(binary, deps) {
255
645
  if (isAbsolute(binary) || binary.includes("/") || binary.includes("\\")) {
256
646
  for (const candidate of candidates) {
257
647
  try {
258
- await deps.access(resolve(candidate), constants.X_OK);
648
+ await deps.access(resolve2(candidate), constants2.X_OK);
259
649
  return true;
260
650
  } catch {
261
651
  continue;
@@ -268,9 +658,9 @@ async function commandExistsOnPath(binary, deps) {
268
658
  continue;
269
659
  }
270
660
  for (const command of candidates) {
271
- const candidate = join(segment, command);
661
+ const candidate = join2(segment, command);
272
662
  try {
273
- await deps.access(candidate, constants.X_OK);
663
+ await deps.access(candidate, constants2.X_OK);
274
664
  return true;
275
665
  } catch {
276
666
  continue;
@@ -330,7 +720,7 @@ async function checkGitInstallation(deps) {
330
720
  }
331
721
  }
332
722
  async function checkWorkflow(repoRoot, deps) {
333
- const workflowPath = join(repoRoot, "WORKFLOW.md");
723
+ const workflowPath = join2(repoRoot, "WORKFLOW.md");
334
724
  let markdown;
335
725
  try {
336
726
  markdown = await deps.readFile(workflowPath, "utf8");
@@ -371,10 +761,140 @@ function buildGithubTrackerConfig(input) {
371
761
  apiUrl: input.projectConfig.projectConfig.tracker.apiUrl,
372
762
  lifecycle: input.workflow.lifecycle,
373
763
  assignedOnly: settings?.assignedOnly === true,
764
+ priority: input.workflow.tracker.priority,
374
765
  priorityFieldName: typeof settings?.priorityFieldName === "string" ? settings.priorityFieldName : void 0,
375
766
  timeoutMs: typeof settings?.timeoutMs === "number" ? settings.timeoutMs : void 0
376
767
  };
377
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
+ }
378
898
  function isActiveSmokeIssue(issue, workflow) {
379
899
  const normalized = issue.state.trim().toLowerCase();
380
900
  return workflow.lifecycle.activeStates.some(
@@ -432,9 +952,9 @@ async function buildHookChecks(repoRoot, workflow, deps) {
432
952
  inline += 1;
433
953
  continue;
434
954
  }
435
- const path = isAbsolute(command) ? command : resolve(repoRoot, command);
955
+ const path = isAbsolute(command) ? command : resolve2(repoRoot, command);
436
956
  try {
437
- await deps.access(path, constants.F_OK);
957
+ await deps.access(path, constants2.F_OK);
438
958
  checked.push({ hook, command, path });
439
959
  } catch {
440
960
  unresolved.push({ hook, command, path });
@@ -447,7 +967,13 @@ async function buildHookChecks(repoRoot, workflow, deps) {
447
967
  "Workflow hook paths",
448
968
  `Unresolved WORKFLOW.md hook path${unresolved.length === 1 ? "" : "s"}: ${unresolved.map((entry) => `${entry.hook}=${entry.command}`).join(", ")}.`,
449
969
  "Create the referenced hook script(s), fix the hook path(s), or replace them with inline commands.",
450
- { 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
+ }
451
977
  )
452
978
  ];
453
979
  }
@@ -458,7 +984,12 @@ async function buildHookChecks(repoRoot, workflow, deps) {
458
984
  "workflow_hooks",
459
985
  "Workflow hook paths",
460
986
  `${pathSummary}${inlineSummary}`,
461
- { configured: configured.length, pathsChecked: checked.length, inline, checked }
987
+ {
988
+ configured: configured.length,
989
+ pathsChecked: checked.length,
990
+ inline,
991
+ checked
992
+ }
462
993
  )
463
994
  ];
464
995
  }
@@ -1145,6 +1676,16 @@ ${DOCTOR_USAGE}`);
1145
1676
  )
1146
1677
  );
1147
1678
  }
1679
+ checks.push(
1680
+ ...await buildPriorityMappingChecks({
1681
+ auth,
1682
+ selection: resolvedProjectConfig,
1683
+ workflow,
1684
+ projectDetail: resolvedGithubProjectDetail,
1685
+ projectBindingId: resolvedGithubProjectBindingId,
1686
+ deps
1687
+ })
1688
+ );
1148
1689
  if (parsedArgs.smoke) {
1149
1690
  checks.push(
1150
1691
  ...await buildDoctorSmokeChecks({
@@ -1268,7 +1809,7 @@ async function ensureDirectoryRemediation(check, deps) {
1268
1809
  }
1269
1810
  try {
1270
1811
  await deps.mkdir(pathValue, { recursive: true });
1271
- await deps.access(pathValue, constants.W_OK);
1812
+ await deps.access(pathValue, constants2.W_OK);
1272
1813
  const target = await deps.stat(pathValue);
1273
1814
  if (!target.isDirectory()) {
1274
1815
  return remediationStep(
@@ -1564,6 +2105,19 @@ function renderTextReport(report) {
1564
2105
  );
1565
2106
  return lines.join("\n");
1566
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
+ }
1567
2121
  async function runDoctorCommand(args, options, dependencies = {}) {
1568
2122
  try {
1569
2123
  const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
@@ -1573,6 +2127,27 @@ async function runDoctorCommand(args, options, dependencies = {}) {
1573
2127
  ${DOCTOR_USAGE}`);
1574
2128
  }
1575
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
+ }
1576
2151
  if (parsedArgs.fix) {
1577
2152
  const remediation = {
1578
2153
  attempted: true,