ghostrail 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +12 -9
  2. package/dist/agent/claude-code.d.ts.map +1 -1
  3. package/dist/agent/claude-code.js +4 -1
  4. package/dist/agent/claude-code.js.map +1 -1
  5. package/dist/board/page.d.ts +1 -1
  6. package/dist/board/page.d.ts.map +1 -1
  7. package/dist/board/page.js +2 -1
  8. package/dist/board/page.js.map +1 -1
  9. package/dist/board/server.d.ts +1 -1
  10. package/dist/board/server.d.ts.map +1 -1
  11. package/dist/board/server.js +7 -1
  12. package/dist/board/server.js.map +1 -1
  13. package/dist/board/stats.d.ts +1 -1
  14. package/dist/board/stats.d.ts.map +1 -1
  15. package/dist/board/stats.js +2 -1
  16. package/dist/board/stats.js.map +1 -1
  17. package/dist/claude-profile/index.d.ts +1 -1
  18. package/dist/claude-profile/index.d.ts.map +1 -1
  19. package/dist/claude-profile/index.js +1 -1
  20. package/dist/claude-profile/index.js.map +1 -1
  21. package/dist/claude-profile/setup.d.ts +7 -0
  22. package/dist/claude-profile/setup.d.ts.map +1 -1
  23. package/dist/claude-profile/setup.js +7 -0
  24. package/dist/claude-profile/setup.js.map +1 -1
  25. package/dist/cli/commands.d.ts +6 -0
  26. package/dist/cli/commands.d.ts.map +1 -1
  27. package/dist/cli/commands.js +204 -18
  28. package/dist/cli/commands.js.map +1 -1
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +3 -1
  31. package/dist/cli.js.map +1 -1
  32. package/dist/commands/help.d.ts.map +1 -1
  33. package/dist/commands/help.js +15 -3
  34. package/dist/commands/help.js.map +1 -1
  35. package/dist/config/load.d.ts.map +1 -1
  36. package/dist/config/load.js +18 -2
  37. package/dist/config/load.js.map +1 -1
  38. package/dist/config/schema.d.ts +8 -2
  39. package/dist/config/schema.d.ts.map +1 -1
  40. package/dist/config/schema.js.map +1 -1
  41. package/dist/dashboard/page.d.ts +7 -1
  42. package/dist/dashboard/page.d.ts.map +1 -1
  43. package/dist/dashboard/page.js +204 -39
  44. package/dist/dashboard/page.js.map +1 -1
  45. package/dist/dashboard/routes.d.ts +32 -0
  46. package/dist/dashboard/routes.d.ts.map +1 -1
  47. package/dist/dashboard/routes.js +45 -0
  48. package/dist/dashboard/routes.js.map +1 -1
  49. package/dist/dashboard/server.d.ts +35 -9
  50. package/dist/dashboard/server.d.ts.map +1 -1
  51. package/dist/dashboard/server.js +75 -11
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/doctor/checks.d.ts +87 -0
  54. package/dist/doctor/checks.d.ts.map +1 -0
  55. package/dist/doctor/checks.js +414 -0
  56. package/dist/doctor/checks.js.map +1 -0
  57. package/dist/doctor/index.d.ts +6 -0
  58. package/dist/doctor/index.d.ts.map +1 -0
  59. package/dist/doctor/index.js +5 -0
  60. package/dist/doctor/index.js.map +1 -0
  61. package/dist/doctor/probes.d.ts +42 -0
  62. package/dist/doctor/probes.d.ts.map +1 -0
  63. package/dist/doctor/probes.js +136 -0
  64. package/dist/doctor/probes.js.map +1 -0
  65. package/dist/doctor/report.d.ts +13 -0
  66. package/dist/doctor/report.d.ts.map +1 -0
  67. package/dist/doctor/report.js +32 -0
  68. package/dist/doctor/report.js.map +1 -0
  69. package/dist/doctor/run.d.ts +48 -0
  70. package/dist/doctor/run.d.ts.map +1 -0
  71. package/dist/doctor/run.js +109 -0
  72. package/dist/doctor/run.js.map +1 -0
  73. package/dist/doctor/types.d.ts +17 -0
  74. package/dist/doctor/types.d.ts.map +1 -0
  75. package/dist/doctor/types.js +6 -0
  76. package/dist/doctor/types.js.map +1 -0
  77. package/dist/factory/build.d.ts.map +1 -1
  78. package/dist/factory/build.js +1 -0
  79. package/dist/factory/build.js.map +1 -1
  80. package/dist/init/regions.d.ts +76 -0
  81. package/dist/init/regions.d.ts.map +1 -0
  82. package/dist/init/regions.js +247 -0
  83. package/dist/init/regions.js.map +1 -0
  84. package/dist/init/template.d.ts +7 -1
  85. package/dist/init/template.d.ts.map +1 -1
  86. package/dist/init/template.js +37 -2
  87. package/dist/init/template.js.map +1 -1
  88. package/dist/loop/budget.d.ts +11 -2
  89. package/dist/loop/budget.d.ts.map +1 -1
  90. package/dist/loop/budget.js +13 -3
  91. package/dist/loop/budget.js.map +1 -1
  92. package/dist/loop/comments.d.ts +20 -0
  93. package/dist/loop/comments.d.ts.map +1 -0
  94. package/dist/loop/comments.js +35 -0
  95. package/dist/loop/comments.js.map +1 -0
  96. package/dist/loop/engine.d.ts.map +1 -1
  97. package/dist/loop/engine.js +18 -0
  98. package/dist/loop/engine.js.map +1 -1
  99. package/dist/loop/index.d.ts +1 -0
  100. package/dist/loop/index.d.ts.map +1 -1
  101. package/dist/loop/index.js +1 -0
  102. package/dist/loop/index.js.map +1 -1
  103. package/dist/publish/gh.d.ts +8 -0
  104. package/dist/publish/gh.d.ts.map +1 -1
  105. package/dist/publish/gh.js +12 -0
  106. package/dist/publish/gh.js.map +1 -1
  107. package/dist/publish/githost.d.ts +8 -0
  108. package/dist/publish/githost.d.ts.map +1 -1
  109. package/dist/publish/githost.js.map +1 -1
  110. package/dist/publish/index.d.ts +1 -1
  111. package/dist/publish/index.d.ts.map +1 -1
  112. package/dist/publish/index.js +1 -1
  113. package/dist/publish/index.js.map +1 -1
  114. package/dist/respond/index.d.ts +3 -2
  115. package/dist/respond/index.d.ts.map +1 -1
  116. package/dist/respond/index.js +3 -2
  117. package/dist/respond/index.js.map +1 -1
  118. package/dist/respond/merge.d.ts +13 -0
  119. package/dist/respond/merge.d.ts.map +1 -0
  120. package/dist/respond/merge.js +181 -0
  121. package/dist/respond/merge.js.map +1 -0
  122. package/dist/respond/respond.d.ts +25 -5
  123. package/dist/respond/respond.d.ts.map +1 -1
  124. package/dist/respond/respond.js +57 -28
  125. package/dist/respond/respond.js.map +1 -1
  126. package/dist/respond/select.d.ts +15 -0
  127. package/dist/respond/select.d.ts.map +1 -1
  128. package/dist/respond/select.js +17 -0
  129. package/dist/respond/select.js.map +1 -1
  130. package/dist/source/dependencies.d.ts +81 -0
  131. package/dist/source/dependencies.d.ts.map +1 -0
  132. package/dist/source/dependencies.js +218 -0
  133. package/dist/source/dependencies.js.map +1 -0
  134. package/dist/source/index.d.ts +1 -0
  135. package/dist/source/index.d.ts.map +1 -1
  136. package/dist/source/index.js +1 -0
  137. package/dist/source/index.js.map +1 -1
  138. package/dist/source/linear.d.ts +17 -1
  139. package/dist/source/linear.d.ts.map +1 -1
  140. package/dist/source/linear.js +75 -9
  141. package/dist/source/linear.js.map +1 -1
  142. package/dist/store/events.d.ts +8 -0
  143. package/dist/store/events.d.ts.map +1 -1
  144. package/dist/store/events.js +10 -0
  145. package/dist/store/events.js.map +1 -1
  146. package/dist/store/file-store.d.ts +1 -1
  147. package/dist/store/file-store.d.ts.map +1 -1
  148. package/dist/store/file-store.js +7 -0
  149. package/dist/store/file-store.js.map +1 -1
  150. package/dist/store/index.d.ts +1 -1
  151. package/dist/store/index.d.ts.map +1 -1
  152. package/dist/store/index.js +1 -1
  153. package/dist/store/index.js.map +1 -1
  154. package/dist/store/port.d.ts +15 -0
  155. package/dist/store/port.d.ts.map +1 -1
  156. package/dist/store/port.js +17 -1
  157. package/dist/store/port.js.map +1 -1
  158. package/dist/triage/engine.d.ts.map +1 -1
  159. package/dist/triage/engine.js +2 -0
  160. package/dist/triage/engine.js.map +1 -1
  161. package/package.json +3 -1
  162. package/skill/ghostrail/SKILL.md +94 -43
  163. package/templates/code/prompts/resolve-issue.md +20 -4
  164. package/templates/code/prompts/respond.md +4 -0
  165. package/templates/code/prompts/triage.md +13 -0
  166. package/templates/content/prompts/draft.md +13 -0
  167. package/templates/content/prompts/respond.md +4 -0
@@ -7,10 +7,11 @@ import { fileURLToPath } from "node:url";
7
7
  import { spawnCollect } from "../backend/proc.js";
8
8
  import { DEFAULT_BOARD_PORT, PORT_FALLBACK_ATTEMPTS, isAddressInUse, isValidPort, portCandidates, } from "../board/port.js";
9
9
  import { createBoardServer, isLoopback, normalizeToken } from "../board/server.js";
10
- import { buildProposal, writeProfile } from "../claude-profile/index.js";
10
+ import { DEFAULT_CLAUDE_PINS, buildProposal, writeProfile } from "../claude-profile/index.js";
11
11
  import { parseDuration } from "../config/duration.js";
12
12
  import { parseConfig, parseMergedConfig } from "../config/load.js";
13
13
  import { createDashboardServer, defaultStateDir } from "../dashboard/index.js";
14
+ import { hasFailure, renderDoctorJson, renderDoctorText, runDoctor, } from "../doctor/index.js";
14
15
  import { buildLoopDeps, buildRespondDeps, buildTriageDeps, repoGhRunner, resolveRepoSlug, } from "../factory/build.js";
15
16
  import { describeMigration, migrateLegacyStore, resolveWorkRepo, stateDirFor, } from "../factory/state.js";
16
17
  import { entryKey, findByPath, findForRepoResolving, replaceEntry, upsert, } from "../global/registry.js";
@@ -18,6 +19,7 @@ import { canonicalRepoId, localRepoId } from "../global/repo-id.js";
18
19
  import { GlobalStore, resolveGlobalDir } from "../global/store.js";
19
20
  import { DEFAULT_FACTORY_IMAGE, dockerBuildArgs, dockerDir, parseImageFlags, } from "../image/build.js";
20
21
  import { extractPlaceholders, isClean, missingSignals, repoPathFor, } from "../init/drift.js";
22
+ import { mergeRegions, parseRegions, regionHashes } from "../init/regions.js";
21
23
  import { classify, hashContent, mergeManifest, readManifest, writeManifest, } from "../init/template.js";
22
24
  import { FileLock } from "../lock/file-lock.js";
23
25
  import { concurrencyAdvice } from "../loop/advice.js";
@@ -430,10 +432,49 @@ export async function cmdBoard(argv, streams, env = process.env, cwd = process.c
430
432
  }
431
433
  /** The cross-ghostrail dashboard's default port, one above the board's. */
432
434
  export const DEFAULT_DASHBOARD_PORT = 5051;
435
+ /**
436
+ * The `owner/repo` slug for a registered ghostrail, for `gh --repo`.
437
+ *
438
+ * The registry's `repo` is the ghostrail's identity and is already canonical
439
+ * (`host/owner/name`), so it answers this without running anything. Two entries
440
+ * cannot: `local:<path>` has no remote by definition, and an entry written
441
+ * before repo identities existed has no `repo` at all. Both fall back to the
442
+ * checkout's origin, which is what `board` does with its one repo.
443
+ *
444
+ * Undefined is not a failure. It leaves `gh` to infer the repo from the
445
+ * checkout, exactly as an unscoped invocation always has.
446
+ */
447
+ async function dashboardRepoSlug(entry) {
448
+ const repo = entry.repo;
449
+ if (repo !== undefined && !repo.startsWith("local:")) {
450
+ const parts = repo.split("/");
451
+ if (parts.length === 3)
452
+ return `${parts[1]}/${parts[2]}`;
453
+ }
454
+ return resolveBoardRepoSlug(entry.path);
455
+ }
433
456
  /** Build the dashboard server's dependencies from the real environment. */
434
457
  function dashboardDeps(env, manager) {
435
458
  const store = globalStoreFor(env);
459
+ // A ghostrail's slug does not change while the dashboard runs, and resolving
460
+ // it can cost a `git` call. The page refetches PR status for every expanded
461
+ // row every 10s, so without this that cost recurs for as long as a row is
462
+ // open. Keyed on the identity, so two clones of one repo share the answer.
463
+ const slugs = new Map();
464
+ const slugFor = (entry) => {
465
+ const key = entryKey(entry);
466
+ const known = slugs.get(key);
467
+ if (known !== undefined)
468
+ return known;
469
+ const resolving = dashboardRepoSlug(entry);
470
+ slugs.set(key, resolving);
471
+ return resolving;
472
+ };
436
473
  return {
474
+ // Constructing this runs nothing; only a request that needs it spends a
475
+ // subprocess. A machine with no `gh` and no credentials still serves the
476
+ // whole dashboard, with PR status reported as unavailable.
477
+ prStatus: async (entry, prNumber) => new GhHost(repoGhRunner(entry.path), await slugFor(entry)).getPrStatus(prNumber),
437
478
  readRegistry: () => store.readRegistry(),
438
479
  writeRegistry: (entries) => store.writeRegistry(entries),
439
480
  health: ghostrailHealth,
@@ -1130,11 +1171,19 @@ async function promptPathsAt(configPath) {
1130
1171
  * `ghostrail init <template> --update`: take the template changes this repo
1131
1172
  * never edited, and name what a customized file is missing.
1132
1173
  *
1133
- * Two jobs split by whether the answer is mechanical. A file still matching its
1134
- * recorded baseline was never touched, so a changed template can overwrite it.
1135
- * A file the repo edited is never written; instead the placeholders and result
1136
- * fields the template gained are named, because that is what every hand port has
1137
- * actually consisted of, and the skill places them in the repo's own wording.
1174
+ * Three jobs, ordered by how much can be proved.
1175
+ *
1176
+ * A file still matching its recorded baseline was never touched, so a changed
1177
+ * template can overwrite it whole.
1178
+ *
1179
+ * A file the repo edited is never overwritten, but the managed regions inside
1180
+ * it can be rewritten in place (TJ-1539): ghostrail owns those outright, so
1181
+ * their current text is irrelevant to producing the new text. A region the repo
1182
+ * edited is left alone and reported, never silently discarded.
1183
+ *
1184
+ * Whatever is left is named, not written: the placeholders and result fields
1185
+ * the template gained, which is what every hand port consisted of before
1186
+ * regions existed, and the skill places them in the repo's own wording.
1138
1187
  */
1139
1188
  async function applyTemplateUpdate(template, srcDir, repo, streams, env) {
1140
1189
  const manifest = await readManifest(repo);
@@ -1145,8 +1194,11 @@ async function applyTemplateUpdate(template, srcDir, repo, streams, env) {
1145
1194
  // compared against: a repo can be current with the release and behind main.
1146
1195
  streams.out(`comparing against ${template} @ ${VERSION}\n\n`);
1147
1196
  const written = {};
1197
+ const writtenRegions = {};
1148
1198
  let pending = 0;
1149
1199
  let took = 0;
1200
+ let mergedRegions = 0;
1201
+ let mergedFiles = 0;
1150
1202
  for (const rel of rels) {
1151
1203
  const key = manifestKey(rel);
1152
1204
  const repoRel = repoPathFor(key, templatePrompts, repoPrompts);
@@ -1180,29 +1232,91 @@ async function applyTemplateUpdate(template, srcDir, repo, streams, env) {
1180
1232
  streams.out(` ${repoRel}\n took the template's version (you had not edited it)\n`);
1181
1233
  continue;
1182
1234
  }
1183
- // Customized, or no baseline to prove otherwise. Never written.
1184
- const signals = missingSignals(current ?? "", templateContent);
1185
- if (isClean(signals)) {
1186
- streams.out(` ${repoRel}\n customized, nothing to take\n`);
1187
- continue;
1235
+ // Customized, or no baseline to prove otherwise. The file as a whole is
1236
+ // never written, but the regions ghostrail owns inside it can be (TJ-1539).
1237
+ let text = current ?? "";
1238
+ const lines = [];
1239
+ const recordedRegions = manifest?.regions?.[key];
1240
+ const merged = mergeRegions(text, templateContent, recordedRegions);
1241
+ if ("error" in merged) {
1242
+ // Malformed markers on either side. Write nothing and fall back to the
1243
+ // signal report, which is always available and always safe.
1244
+ lines.push(` markers unusable: ${merged.error}`);
1245
+ }
1246
+ else if (recordedRegions === undefined && merged.conflicted.length > 0) {
1247
+ // Every region is unprovable for the same one reason, so say it once
1248
+ // rather than per region. Report progress, not just the problem: a
1249
+ // region already matching the template IS recorded by this run, so the
1250
+ // count climbs as regions are ported and the advice cannot loop.
1251
+ //
1252
+ // Pointing at a bare `init` here would loop forever: it keeps a
1253
+ // customized file rather than overwriting it, so it never records a
1254
+ // baseline for the one file that needs one.
1255
+ const adopted = Object.keys(merged.hashes).length;
1256
+ const total = adopted + merged.conflicted.length;
1257
+ lines.push(` adopting regions: ${adopted}/${total} match the template and are recorded`);
1258
+ lines.push(" bring the rest to the template's text and re-run; each one that matches");
1259
+ lines.push(" is recorded, and merges on its own from then on");
1260
+ pending++;
1261
+ if (adopted > 0)
1262
+ writtenRegions[key] = merged.hashes;
1263
+ }
1264
+ else {
1265
+ for (const id of merged.updated)
1266
+ lines.push(` updated ${id}`);
1267
+ for (const id of merged.inserted)
1268
+ lines.push(` inserted ${id}`);
1269
+ for (const id of merged.conflicted) {
1270
+ lines.push(` edited locally ${id} (left alone; port it by hand)`);
1271
+ pending++;
1272
+ }
1273
+ for (const id of merged.unplaceable) {
1274
+ lines.push(` unplaceable ${id} (no shared region to anchor to)`);
1275
+ pending++;
1276
+ }
1277
+ if (merged.updated.length + merged.inserted.length > 0) {
1278
+ text = merged.text;
1279
+ await writeFile(join(repo, repoRel), text, "utf8");
1280
+ mergedRegions += merged.updated.length + merged.inserted.length;
1281
+ mergedFiles++;
1282
+ // Region baselines only. Recording the merged file's whole-file hash
1283
+ // would make `classify` call it `upstream-changed` on the next run,
1284
+ // and that branch overwrites the file, destroying the customization
1285
+ // this path exists to protect.
1286
+ writtenRegions[key] = merged.hashes;
1287
+ }
1288
+ else if (Object.keys(merged.hashes).length > 0) {
1289
+ writtenRegions[key] = merged.hashes;
1290
+ }
1188
1291
  }
1189
- streams.out(` ${repoRel}\n customized, and the template gained:\n`);
1292
+ // Whatever the regions did or could not do, the editable remainder can
1293
+ // still be missing a signal. Read it off the merged text so a placeholder
1294
+ // just delivered by a region is not reported as still missing.
1295
+ const signals = missingSignals(text, templateContent);
1190
1296
  for (const name of signals.placeholders) {
1191
- streams.out(` placeholder {{${name}}}\n`);
1297
+ lines.push(` placeholder {{${name}}}`);
1192
1298
  pending++;
1193
1299
  }
1194
1300
  for (const name of signals.fields) {
1195
- streams.out(` result field "${name}"\n`);
1301
+ lines.push(` result field "${name}"`);
1196
1302
  pending++;
1197
1303
  }
1304
+ if (lines.length === 0) {
1305
+ streams.out(` ${repoRel}\n customized, nothing to take\n`);
1306
+ continue;
1307
+ }
1308
+ streams.out(` ${repoRel}\n customized:\n${lines.join("\n")}\n`);
1198
1309
  }
1199
- if (Object.keys(written).length > 0) {
1200
- await writeManifest(repo, mergeManifest(manifest, template, VERSION, written));
1310
+ if (Object.keys(written).length > 0 || Object.keys(writtenRegions).length > 0) {
1311
+ await writeManifest(repo, mergeManifest(manifest, template, VERSION, written, writtenRegions));
1201
1312
  }
1202
1313
  await registerGhostrail(repo, env, streams);
1203
1314
  streams.out("\n");
1204
1315
  if (took > 0)
1205
1316
  streams.out(`took ${took} file(s) you had not edited\n`);
1317
+ if (mergedRegions > 0) {
1318
+ streams.out(`merged ${mergedRegions} managed region(s) into ${mergedFiles} customized file(s)\n`);
1319
+ }
1206
1320
  if (pending === 0) {
1207
1321
  streams.out("nothing needs placing by hand\n");
1208
1322
  return 0;
@@ -1284,6 +1398,20 @@ export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.
1284
1398
  // current content would later read as "never edited", which is worse than
1285
1399
  // admitting the baseline is unknown.
1286
1400
  const baselines = {};
1401
+ const regionBaselines = {};
1402
+ /**
1403
+ * Record the per-region baselines for a file whose content provably IS the
1404
+ * template's. Without this a repo would customize its editable text, hit the
1405
+ * region path on the next `--update`, find nothing recorded, and be told
1406
+ * every untouched region is a local edit. The merge would then almost never
1407
+ * fire, which is the entire feature.
1408
+ */
1409
+ const recordRegions = (key, content) => {
1410
+ const parsed = parseRegions(content);
1411
+ if (!parsed.ok || parsed.regions.length === 0)
1412
+ return;
1413
+ regionBaselines[key] = regionHashes(parsed.regions);
1414
+ };
1287
1415
  try {
1288
1416
  const existing = await readManifest(repo);
1289
1417
  for (const rel of await scaffoldFiles(srcDir)) {
@@ -1301,6 +1429,7 @@ export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.
1301
1429
  const current = await readIfPresent(to);
1302
1430
  if (current !== undefined && hashContent(current) === templateHash) {
1303
1431
  baselines[key] = templateHash;
1432
+ recordRegions(key, content);
1304
1433
  }
1305
1434
  }
1306
1435
  continue;
@@ -1309,8 +1438,9 @@ export async function cmdInit(argv, streams, cwd = process.cwd(), env = process.
1309
1438
  await writeFile(to, content, "utf8");
1310
1439
  written.push(rel);
1311
1440
  baselines[key] = templateHash;
1441
+ recordRegions(key, content);
1312
1442
  }
1313
- await writeManifest(repo, mergeManifest(existing, template, VERSION, baselines));
1443
+ await writeManifest(repo, mergeManifest(existing, template, VERSION, baselines, regionBaselines));
1314
1444
  }
1315
1445
  catch (error) {
1316
1446
  streams.err(`init failed: ${error instanceof Error ? error.message : String(error)}\n`);
@@ -1675,7 +1805,7 @@ export async function cmdClaudeSetup(argv, streams, env = process.env) {
1675
1805
  home: env.HOME ?? homedir(),
1676
1806
  profile: join(globalDir, "claude"),
1677
1807
  write: false,
1678
- pins: ["testbotpro"],
1808
+ pins: [...DEFAULT_CLAUDE_PINS],
1679
1809
  });
1680
1810
  const proposal = await buildProposal(flags.home, flags.profile, flags.pins);
1681
1811
  streams.out("Claude profile setup\n");
@@ -1820,4 +1950,60 @@ async function openSecretsInEditor(path, flags, streams, env, deps) {
1820
1950
  streams.err(`editor exited ${code}; the file is written. ${fill}`);
1821
1951
  return 0;
1822
1952
  }
1953
+ /** Parse `doctor` flags: the shared `-c/-C` pair, plus `--json` and `--quick`. */
1954
+ function parseDoctorFlags(argv, defaults) {
1955
+ const base = parseCommandFlags(argv, defaults);
1956
+ return {
1957
+ ...base,
1958
+ json: defaults.json || argv.includes("--json"),
1959
+ quick: defaults.quick || argv.includes("--quick"),
1960
+ };
1961
+ }
1962
+ /**
1963
+ * Load `ghostrail.toml` merged with the global settings as a {@link Result},
1964
+ * rather than printing and returning undefined like {@link loadConfig} — unlike
1965
+ * every other command, `doctor` has useful things to say even when config is
1966
+ * broken (credentials, the Claude profile, run history), so it never aborts.
1967
+ */
1968
+ async function loadDoctorConfig(configPath, env) {
1969
+ let repoToml;
1970
+ try {
1971
+ repoToml = await readFile(configPath, "utf8");
1972
+ }
1973
+ catch {
1974
+ return { ok: false, errors: [{ path: "", message: `cannot read config file: ${configPath}` }] };
1975
+ }
1976
+ const globalToml = await globalStoreFor(env).readSettings();
1977
+ return parseMergedConfig(repoToml, globalToml);
1978
+ }
1979
+ /**
1980
+ * `ghostrail doctor`: probe a setup rather than merely inspect it, and name the
1981
+ * fix for whatever is broken. See specs/doctor-command.spec.md.
1982
+ */
1983
+ export async function cmdDoctor(argv, streams, env = process.env, deps = {}) {
1984
+ const flags = parseDoctorFlags(argv, {
1985
+ config: "ghostrail.toml",
1986
+ repo: ".",
1987
+ json: false,
1988
+ quick: false,
1989
+ });
1990
+ const secrets = await applySecrets(flags, env);
1991
+ const configPath = isAbsolute(flags.config) ? flags.config : join(flags.repo, flags.config);
1992
+ const config = await loadDoctorConfig(configPath, env);
1993
+ const claudeProfileDir = await resolveClaudeProfile(env);
1994
+ const state = await stateDirFor(flags.repo, config.ok ? config.value : undefined, env);
1995
+ const historyRows = await new FileStore(state.dir).listRuns();
1996
+ const report = await runDoctor({
1997
+ repoPath: flags.repo,
1998
+ config,
1999
+ env,
2000
+ quick: flags.quick,
2001
+ claudeProfileDir,
2002
+ claudeSourceHome: env.HOME ?? homedir(),
2003
+ historyRows,
2004
+ }, deps);
2005
+ const rendered = flags.json ? renderDoctorJson(report) : renderDoctorText(report);
2006
+ streams.out(redactSecrets(rendered, secrets));
2007
+ return hasFailure(report) ? 1 : 0;
2008
+ }
1823
2009
  //# sourceMappingURL=commands.js.map