@agentskit/doc-bridge 0.1.0-alpha.2 → 1.0.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 (57) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +139 -57
  3. package/action.yml +78 -0
  4. package/dist/cli/program.js +1122 -114
  5. package/dist/cli/program.js.map +1 -1
  6. package/dist/index.d.ts +258 -28
  7. package/dist/index.js +824 -54
  8. package/dist/index.js.map +1 -1
  9. package/docs/DOGFOOD-ROUND2.md +142 -0
  10. package/docs/DOGFOOD-ROUND3.md +74 -0
  11. package/docs/POSITIONING.md +2 -0
  12. package/docs/RELEASE.md +5 -3
  13. package/docs/chat-and-rag.md +2 -0
  14. package/docs/getting-started.md +14 -1
  15. package/docs/landing/index.html +299 -0
  16. package/docs/mcp.md +10 -1
  17. package/docs/ollama-demo.md +64 -0
  18. package/docs/playbook/doc-bridge-pattern.md +114 -0
  19. package/docs/recipes/index-pipeline.md +89 -0
  20. package/docs/skills/doc-bridge.md +64 -0
  21. package/docs/spec/cli.md +6 -0
  22. package/docs/spec/playbook-feedback.md +12 -4
  23. package/examples/demo-example/doc-bridge.config.json +23 -0
  24. package/examples/demo-example/docs/for-agents/INDEX.md +3 -0
  25. package/examples/demo-example/docs/for-agents/packages/example.md +14 -0
  26. package/examples/demo-example/package.json +7 -0
  27. package/examples/demo-example/src/.gitkeep +0 -0
  28. package/examples/demo-monorepo/doc-bridge.config.json +37 -0
  29. package/examples/demo-monorepo/docs/for-agents/INDEX.md +4 -0
  30. package/examples/demo-monorepo/docs/for-agents/packages/auth.md +22 -0
  31. package/examples/demo-monorepo/docs/for-agents/packages/billing.md +19 -0
  32. package/examples/demo-monorepo/docs/human/guides/auth.md +7 -0
  33. package/examples/demo-monorepo/package.json +7 -0
  34. package/examples/demo-monorepo/packages/auth/package.json +8 -0
  35. package/examples/demo-monorepo/packages/billing/package.json +7 -0
  36. package/examples/demo-monorepo/pnpm-workspace.yaml +2 -0
  37. package/examples/ollama-chat.config.ts +42 -0
  38. package/package.json +12 -9
  39. package/src/cli/demo.ts +143 -0
  40. package/src/cli/program.ts +220 -26
  41. package/src/doctor/badge.ts +53 -0
  42. package/src/doctor/run-doctor.ts +286 -0
  43. package/src/federation/llms.ts +20 -11
  44. package/src/index-builder/build-handoffs.ts +35 -2
  45. package/src/index-builder/scan-corpus.ts +5 -1
  46. package/src/index-builder/watch-index.ts +94 -0
  47. package/src/index.ts +24 -0
  48. package/src/lib/markdown.ts +31 -4
  49. package/src/mcp/install.ts +84 -0
  50. package/src/memory/github-pr.ts +190 -0
  51. package/src/playbook/doc-bridge-pattern.ts +121 -0
  52. package/src/query/query.ts +19 -1
  53. package/src/query/search.ts +85 -17
  54. package/src/schemas/agent-handoff.ts +11 -0
  55. package/src/schemas/doc-bridge-index.ts +3 -1
  56. package/src/schemas/json-schemas.ts +2 -1
  57. package/src/version.ts +1 -1
@@ -1,6 +1,6 @@
1
1
  // src/cli/program.ts
2
- import { existsSync as existsSync10, mkdirSync as mkdirSync2, readFileSync as readFileSync15, writeFileSync as writeFileSync2 } from "fs";
3
- import { dirname as dirname4, resolve as resolve6 } from "path";
2
+ import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync17, writeFileSync as writeFileSync4 } from "fs";
3
+ import { dirname as dirname7, resolve as resolve9 } from "path";
4
4
  import { createInterface } from "readline/promises";
5
5
 
6
6
  // src/config/load-config.ts
@@ -469,7 +469,7 @@ var firstHeading = (markdown) => {
469
469
  }
470
470
  return void 0;
471
471
  };
472
- var firstParagraph = (markdown) => {
472
+ var firstParagraph = (markdown, maxLen = 400) => {
473
473
  const { body } = parseFrontmatter(markdown);
474
474
  const lines = body.split("\n");
475
475
  const buf = [];
@@ -481,11 +481,27 @@ var firstParagraph = (markdown) => {
481
481
  }
482
482
  if (t.startsWith("#")) continue;
483
483
  if (t.startsWith("---")) continue;
484
+ if (t.startsWith("```")) break;
485
+ if (t.startsWith("|") || t.startsWith("- [") || t.startsWith("* [")) {
486
+ if (buf.length) break;
487
+ continue;
488
+ }
484
489
  buf.push(t);
485
- if (buf.join(" ").length > 40) break;
486
- }
487
- const text = buf.join(" ").trim();
488
- return text || void 0;
490
+ if (buf.join(" ").length >= maxLen) break;
491
+ }
492
+ let text = buf.join(" ").replace(/\s+/g, " ").trim();
493
+ if (!text) return void 0;
494
+ if (text.length <= maxLen) return text;
495
+ const sliced = text.slice(0, maxLen);
496
+ const sentenceEnd = Math.max(sliced.lastIndexOf(". "), sliced.lastIndexOf("! "), sliced.lastIndexOf("? "));
497
+ if (sentenceEnd > maxLen * 0.4) return sliced.slice(0, sentenceEnd + 1).trim();
498
+ const wordEnd = sliced.lastIndexOf(" ");
499
+ return (wordEnd > 0 ? sliced.slice(0, wordEnd) : sliced).trim();
500
+ };
501
+ var extractSearchBody = (markdown, maxLen = 6e3) => {
502
+ const { body } = parseFrontmatter(markdown);
503
+ const text = body.replace(/```[\s\S]*?```/g, " ").replace(/!\[[^\]]*\]\([^)]+\)/g, " ").replace(/\[[^\]]*\]\([^)]+\)/g, " ").replace(/^#+\s+/gm, "").replace(/[|>*_`#]/g, " ").replace(/\s+/g, " ").trim();
504
+ return text.length > maxLen ? text.slice(0, maxLen) : text;
489
505
  };
490
506
  var slugFromPath = (relPath) => {
491
507
  const base = relPath.replace(/\.mdx?$/, "");
@@ -543,7 +559,9 @@ var scanAgentCorpus = (root, config) => {
543
559
  const { data: frontmatter } = parseFrontmatter(raw);
544
560
  const id = frontmatterString(frontmatter, "id") ?? frontmatterString(frontmatter, "package") ?? slugFromPath(relToCorpus);
545
561
  const title = firstHeading(raw) ?? id;
546
- const description = firstParagraph(raw);
562
+ const purpose = frontmatterString(frontmatter, "purpose");
563
+ const description = purpose ?? firstParagraph(raw, 400);
564
+ const body = extractSearchBody(raw);
547
565
  return {
548
566
  id,
549
567
  type: "agent-doc",
@@ -552,7 +570,8 @@ var scanAgentCorpus = (root, config) => {
552
570
  absPath: abs,
553
571
  relPath,
554
572
  frontmatter,
555
- ...description ? { description } : {}
573
+ ...description ? { description } : {},
574
+ ...body ? { body } : {}
556
575
  };
557
576
  });
558
577
  };
@@ -685,11 +704,19 @@ var resolveHumanDoc = (packageId, override, fmHuman, humanDocs = {}) => {
685
704
  packageId,
686
705
  packageId.replace(/^@[^/]+\//, ""),
687
706
  packageId.replace(/^os-/, ""),
688
- packageId.replace(/-pattern$/, "")
707
+ packageId.replace(/-pattern$/, ""),
708
+ `packages/${packageId}`,
709
+ `reference/packages/${packageId}`,
710
+ `packages/${packageId}/index`
689
711
  ];
690
712
  for (const alias of aliases) {
691
713
  if (humanDocs[alias]) return humanDocs[alias];
692
714
  }
715
+ for (const [key, url] of Object.entries(humanDocs)) {
716
+ if (key === packageId || key.endsWith(`/${packageId}`) || key.endsWith(`/${packageId}/index`) || key.endsWith(`-${packageId}`)) {
717
+ return url;
718
+ }
719
+ }
693
720
  return void 0;
694
721
  };
695
722
  var buildLookup = (config, packages, corpus, indexOutFile, humanDocs = {}, root = process.cwd()) => {
@@ -729,6 +756,11 @@ var buildLookup = (config, packages, corpus, indexOutFile, humanDocs = {}, root
729
756
  ...agentDoc ? { agentDoc } : {}
730
757
  };
731
758
  ownership[pkg.id] = record;
759
+ const bridge = record.humanDoc ? /^https?:\/\//.test(record.humanDoc) ? { humanDoc: "external" } : { humanDoc: "linked" } : config.corpus.human ? {
760
+ humanDoc: "missing",
761
+ action: "ak-docs bootstrap agent-docs",
762
+ bootstrap: `docs/for-agents/human/${pkg.id}.md`
763
+ } : void 0;
732
764
  handoffs[pkg.id] = {
733
765
  type: "agent-handoff",
734
766
  schemaVersion: 1,
@@ -747,7 +779,11 @@ var buildLookup = (config, packages, corpus, indexOutFile, humanDocs = {}, root
747
779
  editRoots: [record.path],
748
780
  checks: [...record.checks],
749
781
  ...record.humanDoc ? { humanDoc: record.humanDoc } : {},
750
- notes: record.purpose ? [record.purpose] : []
782
+ ...bridge ? { bridge } : {},
783
+ notes: [
784
+ ...record.purpose ? [record.purpose] : [],
785
+ ...!record.humanDoc && config.corpus.human ? [`Human guide missing for ${pkg.id}. Run: ak-docs bootstrap agent-docs`] : []
786
+ ]
751
787
  };
752
788
  }
753
789
  const intents = {};
@@ -775,7 +811,7 @@ var buildLookup = (config, packages, corpus, indexOutFile, humanDocs = {}, root
775
811
  };
776
812
 
777
813
  // src/version.ts
778
- var PACKAGE_VERSION = "0.1.0-alpha.2";
814
+ var PACKAGE_VERSION = "1.0.0";
779
815
 
780
816
  // src/index-builder/capabilities.ts
781
817
  var renderCapabilitiesJson = (config, index, paths) => {
@@ -874,10 +910,10 @@ var docId = (relToHumanRoot, raw) => {
874
910
  return meta.package ?? meta.module ?? meta.id ?? slugFromPath(relToHumanRoot);
875
911
  };
876
912
  var routeSlug = (relToHumanRoot, opts) => relToHumanRoot.split("/").filter((part) => !opts?.stripGroups || !/^\(.+\)$/.test(part)).join("/").replace(/(?:^|\/)index\.mdx?$/, "").replace(/\.mdx?$/, "");
877
- var humanUrl = (slug, urlPrefix) => {
878
- if (typeof urlPrefix !== "string" || !urlPrefix) return slug;
913
+ var humanUrl = (slug2, urlPrefix) => {
914
+ if (typeof urlPrefix !== "string" || !urlPrefix) return slug2;
879
915
  const prefix = urlPrefix.replace(/\/$/, "");
880
- return slug ? `${prefix}/${slug.replace(/^\//, "")}` : prefix;
916
+ return slug2 ? `${prefix}/${slug2.replace(/^\//, "")}` : prefix;
881
917
  };
882
918
  var scanMarkdownDocs = (root, humanRoot, options) => {
883
919
  const out = [];
@@ -908,9 +944,9 @@ var docusaurusFileSlug = (relPath) => {
908
944
  return routeSlug(relPath);
909
945
  };
910
946
  var docusaurusSlug = (relPath, raw) => {
911
- const slug = parseFrontmatter2(raw).slug;
912
- if (!slug) return docusaurusFileSlug(relPath);
913
- return slug.replace(/^\.\//, "").replace(/^\//, "").replace(/\/$/, "");
947
+ const slug2 = parseFrontmatter2(raw).slug;
948
+ if (!slug2) return docusaurusFileSlug(relPath);
949
+ return slug2.replace(/^\.\//, "").replace(/^\//, "").replace(/\/$/, "");
914
950
  };
915
951
  var docusaurusSidebarId = (relPath, raw) => {
916
952
  const id = parseFrontmatter2(raw).id;
@@ -1252,19 +1288,58 @@ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
1252
1288
  import { resolve as resolve3 } from "path";
1253
1289
 
1254
1290
  // src/query/search.ts
1255
- var tokenize = (value) => value.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 2);
1291
+ var tokenize = (value) => value.toLowerCase().split(/[^a-z0-9@/_-]+/).filter((t) => t.length >= 2);
1292
+ var PACKAGE_INTENT = /\b(package|module|pkg|edit|change|where|owns?|ownership|handoff|start)\b/i;
1293
+ var scoreHay = (tokens, hay, weight = 1) => {
1294
+ let score = 0;
1295
+ for (const token of tokens) {
1296
+ if (!hay.includes(token)) continue;
1297
+ score += token.length * weight;
1298
+ if (new RegExp(`(?:^|[^a-z0-9])${token}(?:[^a-z0-9]|$)`).test(hay)) {
1299
+ score += token.length;
1300
+ }
1301
+ }
1302
+ return score;
1303
+ };
1304
+ var identityBoost = (id, path, tokens, term) => {
1305
+ const idLower = id.toLowerCase();
1306
+ const termLower = term.toLowerCase().trim();
1307
+ const base = path.split("/").pop()?.replace(/\.mdx?$/i, "").toLowerCase() ?? "";
1308
+ let boost = 0;
1309
+ if (idLower === termLower || base === termLower) boost += 200;
1310
+ if (tokens.length === 1 && (idLower === tokens[0] || base === tokens[0])) boost += 200;
1311
+ for (const token of tokens) {
1312
+ if (idLower === token) boost += 120;
1313
+ else if (idLower.startsWith(`${token}-`) || idLower.endsWith(`-${token}`)) boost += 40;
1314
+ else if (idLower.includes(token) && idLower.length <= token.length + 4) boost += 30;
1315
+ if (base === token) boost += 100;
1316
+ }
1317
+ if (tokens.includes(idLower)) boost += Math.max(0, 40 - idLower.length);
1318
+ return boost;
1319
+ };
1320
+ var preferOwnership = (term) => PACKAGE_INTENT.test(term) || /^(where|how).*(edit|change|package|module)/i.test(term);
1256
1321
  var searchIndex = (index, term, limit = 20) => {
1257
1322
  const tokens = tokenize(term);
1258
1323
  if (!tokens.length) return [];
1259
- const matches = [];
1260
- for (const entry of index.knowledge) {
1261
- const hay = `${entry.id} ${entry.title} ${entry.description ?? ""} ${entry.path}`.toLowerCase();
1262
- let score = 0;
1263
- for (const token of tokens) {
1264
- if (hay.includes(token)) score += token.length;
1324
+ const wantOwnership = preferOwnership(term);
1325
+ const byPath = /* @__PURE__ */ new Map();
1326
+ const consider = (match) => {
1327
+ const key = match.path;
1328
+ const existing = byPath.get(key);
1329
+ if (!existing) {
1330
+ byPath.set(key, match);
1331
+ return;
1265
1332
  }
1333
+ const prefer = match.score > existing.score || match.score === existing.score && match.type === "ownership" && existing.type !== "ownership" || wantOwnership && match.type === "ownership" && existing.type !== "ownership" && match.score >= existing.score - 20;
1334
+ if (prefer) byPath.set(key, match);
1335
+ };
1336
+ for (const entry of index.knowledge) {
1337
+ const body = entry.body ?? "";
1338
+ const hay = `${entry.id} ${entry.title} ${entry.description ?? ""} ${entry.path} ${body}`.toLowerCase();
1339
+ let score = scoreHay(tokens, hay, 1);
1340
+ score += identityBoost(entry.id, entry.path, tokens, term);
1266
1341
  if (score > 0) {
1267
- matches.push({
1342
+ consider({
1268
1343
  type: "knowledge",
1269
1344
  id: entry.id,
1270
1345
  path: entry.path,
@@ -1274,22 +1349,31 @@ var searchIndex = (index, term, limit = 20) => {
1274
1349
  }
1275
1350
  }
1276
1351
  for (const [id, owner] of Object.entries(index.lookup?.ownership ?? {})) {
1277
- const hay = `${id} ${owner.path} ${owner.purpose ?? ""} ${owner.group ?? ""}`.toLowerCase();
1278
- let score = 0;
1279
- for (const token of tokens) {
1280
- if (hay.includes(token)) score += token.length * 2;
1281
- }
1352
+ const path = owner.agentDoc ?? owner.path;
1353
+ const hay = `${id} ${owner.path} ${owner.purpose ?? ""} ${owner.group ?? ""} ${owner.agentDoc ?? ""} ${owner.humanDoc ?? ""}`.toLowerCase();
1354
+ let score = scoreHay(tokens, hay, 2);
1355
+ score += identityBoost(id, path, tokens, term);
1356
+ if (wantOwnership) score += 25;
1357
+ score += 15;
1282
1358
  if (score > 0) {
1283
- matches.push({
1359
+ consider({
1284
1360
  type: "ownership",
1285
1361
  id,
1286
- path: owner.agentDoc ?? owner.path,
1362
+ path,
1287
1363
  ...owner.purpose ? { summary: owner.purpose } : {},
1288
1364
  score
1289
1365
  });
1290
1366
  }
1291
1367
  }
1292
- return matches.sort((a, b) => b.score - a.score).slice(0, limit);
1368
+ return [...byPath.values()].sort((a, b) => {
1369
+ if (b.score !== a.score) return b.score - a.score;
1370
+ const aExact = tokens.includes(a.id.toLowerCase()) ? 1 : 0;
1371
+ const bExact = tokens.includes(b.id.toLowerCase()) ? 1 : 0;
1372
+ if (bExact !== aExact) return bExact - aExact;
1373
+ if (a.type === "ownership" && b.type !== "ownership") return -1;
1374
+ if (b.type === "ownership" && a.type !== "ownership") return 1;
1375
+ return a.id.localeCompare(b.id);
1376
+ }).slice(0, limit);
1293
1377
  };
1294
1378
 
1295
1379
  // src/retriever/doc-bridge-retriever.ts
@@ -1328,10 +1412,14 @@ var defaultFetchText = async (url) => {
1328
1412
  return res.text();
1329
1413
  };
1330
1414
  var sourceText = async (root, source, fetchText) => {
1331
- if (/^https?:\/\//.test(source)) return fetchText(source);
1332
- const path = resolve3(root, source);
1333
- if (!existsSync7(path)) throw new Error(`Federation source not found: ${source}`);
1334
- return readFileSync9(path, "utf8");
1415
+ try {
1416
+ if (/^https?:\/\//.test(source)) return await fetchText(source);
1417
+ const path = resolve3(root, source);
1418
+ if (!existsSync7(path)) return null;
1419
+ return readFileSync9(path, "utf8");
1420
+ } catch {
1421
+ return null;
1422
+ }
1335
1423
  };
1336
1424
  var sameOrigin = (base, target) => {
1337
1425
  if (!/^https?:\/\//.test(base) || !/^https?:\/\//.test(target)) return true;
@@ -1370,22 +1458,28 @@ var chunksFromMarkdown = (property, raw, sourceUrl) => {
1370
1458
  var loadFederatedChunks = async (root, config, options = {}) => {
1371
1459
  const fetchText = options.fetchText ?? defaultFetchText;
1372
1460
  const chunks = [];
1461
+ const warnings = [];
1373
1462
  for (const source of config.federation?.sources ?? []) {
1374
1463
  if (source.includeInRetriever === false || !source.llmsTxt) continue;
1375
1464
  const llms = await sourceText(root, source.llmsTxt, fetchText);
1465
+ if (!llms) {
1466
+ warnings.push(`federation source skipped (unavailable): ${source.id} \u2192 ${source.llmsTxt}`);
1467
+ continue;
1468
+ }
1376
1469
  chunks.push(...chunksFromMarkdown(source.id, llms, source.llmsTxt));
1377
1470
  const links = parseLlmsTxtLinks(llms);
1378
1471
  for (const link of links) {
1379
1472
  const url = !/^https?:\/\//.test(link.url) && source.rawBaseUrl ? `${source.rawBaseUrl.replace(/\/$/, "")}/${link.url.replace(/^\//, "")}` : link.url;
1380
1473
  if (!/\.(md|txt)(?:$|\?)/.test(url)) continue;
1381
1474
  if (!sameOrigin(source.llmsTxt, url)) continue;
1382
- try {
1383
- const raw = await sourceText(root, url, fetchText);
1384
- chunks.push(...chunksFromMarkdown(source.id, raw, url));
1385
- } catch {
1386
- }
1475
+ const raw = await sourceText(root, url, fetchText);
1476
+ if (raw) chunks.push(...chunksFromMarkdown(source.id, raw, url));
1387
1477
  }
1388
1478
  }
1479
+ if (warnings.length && process.stderr.isTTY) {
1480
+ for (const w of warnings) process.stderr.write(`${w}
1481
+ `);
1482
+ }
1389
1483
  return chunks;
1390
1484
  };
1391
1485
  var retrieveHybridChunks = async (root, config, index, query, options = {}) => {
@@ -1434,6 +1528,11 @@ var HandoffTargetSchema = z2.object({
1434
1528
  group: z2.string().min(1).max(128).optional(),
1435
1529
  layer: z2.string().min(1).max(32).optional()
1436
1530
  }).strict();
1531
+ var HandoffBridgeSchema = z2.object({
1532
+ humanDoc: z2.enum(["linked", "missing", "external"]),
1533
+ action: z2.string().min(1).max(256).optional(),
1534
+ bootstrap: z2.string().min(1).max(256).optional()
1535
+ }).strict();
1437
1536
  var AgentHandoffV1Schema = z2.object({
1438
1537
  type: z2.literal("agent-handoff"),
1439
1538
  schemaVersion: z2.literal(HANDOFF_SCHEMA_VERSION).default(HANDOFF_SCHEMA_VERSION),
@@ -1444,6 +1543,7 @@ var AgentHandoffV1Schema = z2.object({
1444
1543
  editRoots: z2.array(z2.string().min(1).max(512)).max(32),
1445
1544
  checks: z2.array(z2.string().min(1).max(256)).max(32),
1446
1545
  humanDoc: z2.string().min(1).max(512).nullable().optional(),
1546
+ bridge: HandoffBridgeSchema.optional(),
1447
1547
  playbookPatterns: z2.array(z2.string().url()).max(16).optional(),
1448
1548
  notes: z2.array(z2.string().min(1).max(1024)).max(16)
1449
1549
  }).strict();
@@ -1485,7 +1585,9 @@ var KnowledgeEntrySchema = z3.object({
1485
1585
  type: z3.string().min(1).max(128),
1486
1586
  title: z3.string().min(1).max(256),
1487
1587
  path: z3.string().min(1).max(512),
1488
- description: z3.string().max(1024).optional(),
1588
+ description: z3.string().max(2048).optional(),
1589
+ /** Flattened body excerpt for full-text search (not for display). */
1590
+ body: z3.string().max(8e3).optional(),
1489
1591
  links: z3.array(z3.string().min(1).max(512)).max(64).optional(),
1490
1592
  tags: z3.array(z3.string().min(1).max(64)).max(32).optional()
1491
1593
  }).strict();
@@ -1802,6 +1904,11 @@ var handoffForPackage = (index, id, config) => {
1802
1904
  if (fromIndex) return normalizeAgentHandoff(fromIndex);
1803
1905
  const owner = index.lookup?.ownership?.[id];
1804
1906
  if (!owner) throw new Error(`Unknown package/ownership id "${id}". Try: ak-docs list packages`);
1907
+ const bridge = owner.humanDoc ? /^https?:\/\//.test(owner.humanDoc) ? { humanDoc: "external" } : { humanDoc: "linked" } : config.corpus.human ? {
1908
+ humanDoc: "missing",
1909
+ action: "ak-docs bootstrap agent-docs",
1910
+ bootstrap: `docs/for-agents/human/${id}.md`
1911
+ } : void 0;
1805
1912
  return normalizeAgentHandoff({
1806
1913
  type: "agent-handoff",
1807
1914
  source: config.index?.outFile ?? ".doc-bridge/index.json",
@@ -1817,7 +1924,11 @@ var handoffForPackage = (index, id, config) => {
1817
1924
  editRoots: [owner.path],
1818
1925
  checks: [...owner.checks],
1819
1926
  ...owner.humanDoc ? { humanDoc: owner.humanDoc } : {},
1820
- notes: owner.purpose ? [owner.purpose] : []
1927
+ ...bridge ? { bridge } : {},
1928
+ notes: [
1929
+ ...owner.purpose ? [owner.purpose] : [],
1930
+ ...!owner.humanDoc && config.corpus.human ? [`Human guide missing for ${id}. Run: ak-docs bootstrap agent-docs`] : []
1931
+ ]
1821
1932
  });
1822
1933
  };
1823
1934
  var runQuery = (index, config, req) => {
@@ -2290,9 +2401,752 @@ var draftMemoryPromotion = (classifications) => {
2290
2401
  };
2291
2402
  };
2292
2403
 
2404
+ // src/memory/github-pr.ts
2405
+ import { execFileSync, spawnSync } from "child_process";
2406
+ import { existsSync as existsSync10, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
2407
+ import { join as join15 } from "path";
2408
+ var run = (cmd, args, cwd) => {
2409
+ const result = spawnSync(cmd, [...args], { cwd, encoding: "utf8" });
2410
+ const out = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
2411
+ return { ok: result.status === 0, out };
2412
+ };
2413
+ var hasGh = () => run("gh", ["--version"], process.cwd()).ok;
2414
+ var slug = () => (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
2415
+ var defaultPromotionDraftPath = (root) => join15(root, ".doc-bridge", "drafts", `memory-promotion-${slug()}.md`);
2416
+ var writePromotionDraft = (root, draft, path) => {
2417
+ const draftPath = path ?? defaultPromotionDraftPath(root);
2418
+ mkdirSync2(join15(root, ".doc-bridge", "drafts"), { recursive: true });
2419
+ writeFileSync2(draftPath, `${draft.body}
2420
+ `, "utf8");
2421
+ return draftPath;
2422
+ };
2423
+ var promoteMemoryToGithubPr = (root, draft, options = {}) => {
2424
+ if (!draft.ok && !options.force) {
2425
+ return {
2426
+ ok: false,
2427
+ dryRun: Boolean(options.dryRun),
2428
+ draftPath: "",
2429
+ branch: "",
2430
+ commands: [],
2431
+ message: "Safety scan blocked promotion. Fix findings or pass --force to draft anyway."
2432
+ };
2433
+ }
2434
+ const branch = options.branch ?? `doc-bridge/memory-promotion-${slug()}`;
2435
+ const draftPath = writePromotionDraft(root, draft);
2436
+ const relDraft = draftPath.startsWith(`${root}/`) ? draftPath.slice(root.length + 1) : draftPath;
2437
+ const commands = [
2438
+ `git checkout -b ${branch}`,
2439
+ `git add ${relDraft}`,
2440
+ `git commit -m "draft: doc-bridge memory promotion"`,
2441
+ `git push -u origin ${branch}`,
2442
+ `gh pr create --draft --title "${draft.title}" --body-file ${relDraft}${options.base ? ` --base ${options.base}` : ""}`
2443
+ ];
2444
+ if (options.dryRun) {
2445
+ return {
2446
+ ok: true,
2447
+ dryRun: true,
2448
+ draftPath,
2449
+ branch,
2450
+ commands,
2451
+ message: `Wrote draft to ${relDraft}. Run the printed git/gh commands to open a draft PR.`
2452
+ };
2453
+ }
2454
+ if (!existsSync10(join15(root, ".git"))) {
2455
+ return {
2456
+ ok: false,
2457
+ dryRun: false,
2458
+ draftPath,
2459
+ branch,
2460
+ commands,
2461
+ message: "Not a git repository. Commit the draft manually or run with --dry-run."
2462
+ };
2463
+ }
2464
+ if (!hasGh()) {
2465
+ return {
2466
+ ok: false,
2467
+ dryRun: false,
2468
+ draftPath,
2469
+ branch,
2470
+ commands,
2471
+ message: "GitHub CLI (gh) not found. Install gh or use --dry-run for local draft + commands."
2472
+ };
2473
+ }
2474
+ const auth = run("gh", ["auth", "status"], root);
2475
+ if (!auth.ok) {
2476
+ return {
2477
+ ok: false,
2478
+ dryRun: false,
2479
+ draftPath,
2480
+ branch,
2481
+ commands,
2482
+ message: `gh is not authenticated. Run: gh auth login
2483
+ ${auth.out}`
2484
+ };
2485
+ }
2486
+ const checkout = run("git", ["checkout", "-b", branch], root);
2487
+ if (!checkout.ok) {
2488
+ return {
2489
+ ok: false,
2490
+ dryRun: false,
2491
+ draftPath,
2492
+ branch,
2493
+ commands,
2494
+ message: `git checkout failed: ${checkout.out}`
2495
+ };
2496
+ }
2497
+ run("git", ["add", relDraft], root);
2498
+ const commit = run("git", ["commit", "-m", "draft: doc-bridge memory promotion"], root);
2499
+ if (!commit.ok) {
2500
+ return {
2501
+ ok: false,
2502
+ dryRun: false,
2503
+ draftPath,
2504
+ branch,
2505
+ commands,
2506
+ message: `git commit failed: ${commit.out}`
2507
+ };
2508
+ }
2509
+ const push = run("git", ["push", "-u", "origin", branch], root);
2510
+ if (!push.ok) {
2511
+ return {
2512
+ ok: false,
2513
+ dryRun: false,
2514
+ draftPath,
2515
+ branch,
2516
+ commands,
2517
+ message: `git push failed: ${push.out}`
2518
+ };
2519
+ }
2520
+ const prArgs = [
2521
+ "pr",
2522
+ "create",
2523
+ "--draft",
2524
+ "--title",
2525
+ draft.title,
2526
+ "--body-file",
2527
+ relDraft,
2528
+ ...options.base ? ["--base", options.base] : []
2529
+ ];
2530
+ let prUrl = "";
2531
+ try {
2532
+ prUrl = execFileSync("gh", prArgs, { cwd: root, encoding: "utf8" }).trim();
2533
+ } catch (error) {
2534
+ const message = error instanceof Error ? error.message : String(error);
2535
+ return {
2536
+ ok: false,
2537
+ dryRun: false,
2538
+ draftPath,
2539
+ branch,
2540
+ commands,
2541
+ message: `gh pr create failed: ${message}`
2542
+ };
2543
+ }
2544
+ return {
2545
+ ok: true,
2546
+ dryRun: false,
2547
+ draftPath,
2548
+ branch,
2549
+ prUrl,
2550
+ ...prUrl ? { previewUrl: prUrl } : {},
2551
+ commands,
2552
+ message: prUrl ? `Draft PR opened: ${prUrl}` : "Draft PR created."
2553
+ };
2554
+ };
2555
+
2556
+ // src/index-builder/watch-index.ts
2557
+ import { existsSync as existsSync11, watch } from "fs";
2558
+ import { dirname as dirname4, resolve as resolve5 } from "path";
2559
+ var WATCH_PATTERN = /\.(md|mdx|json|ya?ml|mdc)$/i;
2560
+ var collectWatchRoots = (root, config, configPath) => {
2561
+ const roots = /* @__PURE__ */ new Set();
2562
+ roots.add(resolve5(root, config.corpus.agent.root));
2563
+ const humanSources = config.corpus.human ? Array.isArray(config.corpus.human) ? config.corpus.human : [config.corpus.human] : [];
2564
+ for (const source of humanSources) {
2565
+ const humanOpts = source.options ?? {};
2566
+ for (const key of ["contentDir", "docsDir", "root"]) {
2567
+ const value = humanOpts[key];
2568
+ if (typeof value === "string" && value.length) roots.add(resolve5(root, value));
2569
+ }
2570
+ }
2571
+ if (configPath) roots.add(dirname4(resolve5(configPath)));
2572
+ return [...roots].filter((dir) => existsSync11(dir));
2573
+ };
2574
+ var watchDocBridgeIndex = (opts) => {
2575
+ const debounceMs = opts.debounceMs ?? 350;
2576
+ let timer;
2577
+ let running = false;
2578
+ const rebuild = () => {
2579
+ if (timer) clearTimeout(timer);
2580
+ timer = setTimeout(() => {
2581
+ if (running) return;
2582
+ running = true;
2583
+ try {
2584
+ const result = buildDocBridgeIndex({ root: opts.root, config: opts.config });
2585
+ const handoffCount = Object.keys(result.index.handoffs ?? {}).length;
2586
+ const summary = {
2587
+ knowledgeCount: result.index.knowledge.length,
2588
+ handoffCount,
2589
+ hash: result.index.contentHash.slice(0, 8)
2590
+ };
2591
+ opts.onRebuild?.(summary);
2592
+ process.stdout.write(
2593
+ `[ak-docs] indexed ${summary.knowledgeCount} docs, ${summary.handoffCount} handoffs (${summary.hash}\u2026)
2594
+ `
2595
+ );
2596
+ } catch (error) {
2597
+ process.stderr.write(
2598
+ `[ak-docs] index failed: ${error instanceof Error ? error.message : String(error)}
2599
+ `
2600
+ );
2601
+ } finally {
2602
+ running = false;
2603
+ }
2604
+ }, debounceMs);
2605
+ };
2606
+ rebuild();
2607
+ for (const dir of collectWatchRoots(opts.root, opts.config, opts.configPath)) {
2608
+ watch(dir, { recursive: true }, (_event, filename) => {
2609
+ if (!filename || !WATCH_PATTERN.test(filename)) return;
2610
+ rebuild();
2611
+ });
2612
+ }
2613
+ const configDir = resolve5(opts.root);
2614
+ if (existsSync11(configDir)) {
2615
+ watch(configDir, (_event, filename) => {
2616
+ if (!filename || !/doc-bridge\.config/.test(filename)) return;
2617
+ rebuild();
2618
+ });
2619
+ }
2620
+ process.stdout.write(
2621
+ `[ak-docs] watching ${collectWatchRoots(opts.root, opts.config, opts.configPath).join(", ") || opts.root} (Ctrl+C to stop)
2622
+ `
2623
+ );
2624
+ return new Promise((resolvePromise) => {
2625
+ const onSignal = () => resolvePromise(0);
2626
+ process.once("SIGINT", onSignal);
2627
+ process.once("SIGTERM", onSignal);
2628
+ });
2629
+ };
2630
+
2631
+ // src/doctor/badge.ts
2632
+ var doctorBadgeMetrics = (report) => {
2633
+ const { packages } = report.coverage;
2634
+ const handoffPct = packages.total > 0 ? Math.round(packages.withAgentDoc / packages.total * 100) : 0;
2635
+ const bridgePct = packages.total > 0 ? Math.round(packages.withHumanDoc / packages.total * 100) : 0;
2636
+ return {
2637
+ handoffPct,
2638
+ bridgePct,
2639
+ score: report.score,
2640
+ grade: report.grade,
2641
+ packages: packages.total
2642
+ };
2643
+ };
2644
+ var badgeColor = (pct) => {
2645
+ if (pct >= 80) return "2ea44f";
2646
+ if (pct >= 50) return "dbab09";
2647
+ return "cb2431";
2648
+ };
2649
+ var formatDoctorBadgeMarkdown = (metrics) => {
2650
+ const handoff = `handoff_coverage-${metrics.handoffPct}%25-${badgeColor(metrics.handoffPct)}`;
2651
+ const bridge = `human_bridge-${metrics.bridgePct}%25-${badgeColor(metrics.bridgePct)}`;
2652
+ return [
2653
+ `![handoff coverage](https://img.shields.io/badge/${handoff}?style=flat-square)`,
2654
+ `![human bridge](https://img.shields.io/badge/${bridge}?style=flat-square)`,
2655
+ `![doc-bridge score](https://img.shields.io/badge/doc--bridge_score-${metrics.score}%2F100-${badgeColor(metrics.score)}?style=flat-square)`
2656
+ ].join(" ");
2657
+ };
2658
+ var formatDoctorBadgeJson = (metrics) => JSON.stringify(
2659
+ {
2660
+ schemaVersion: 1,
2661
+ ...metrics,
2662
+ markdown: formatDoctorBadgeMarkdown(metrics),
2663
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2664
+ },
2665
+ null,
2666
+ 2
2667
+ );
2668
+
2669
+ // src/playbook/doc-bridge-pattern.ts
2670
+ var DOC_BRIDGE_PATTERN_ID = "doc-bridge-pattern";
2671
+ var DOC_BRIDGE_PATTERN_META = {
2672
+ id: DOC_BRIDGE_PATTERN_ID,
2673
+ title: "Doc Bridge Pattern",
2674
+ slug: "pillars/ai-collaboration/doc-bridge-pattern",
2675
+ license: "CC-BY-4.0",
2676
+ visibility: "public",
2677
+ playbookUrl: "https://playbook.agentskit.io/patterns/doc-bridge-pattern",
2678
+ npmPackage: "@agentskit/doc-bridge",
2679
+ cli: "ak-docs"
2680
+ };
2681
+ var docBridgePatternMarkdown = () => `---
2682
+ type: pattern
2683
+ id: ${DOC_BRIDGE_PATTERN_ID}
2684
+ purpose: Route coding agents to the correct package, checks, and human docs in any monorepo.
2685
+ owner: AgentsKit
2686
+ license: CC-BY-4.0
2687
+ visibility: public
2688
+ tags: [agents, documentation, monorepo, handoff, mcp]
2689
+ ---
2690
+
2691
+ # Doc Bridge Pattern
2692
+
2693
+ **AgentHandoff for your monorepo** \u2014 deterministic routing so agents edit the right roots, run the right checks, and stay linked to human documentation.
2694
+
2695
+ ## Problem
2696
+
2697
+ Coding agents guess package ownership. They edit sibling modules, run repo-wide tests, and ship changes without a bridge to human-facing guides. Wikis and RAG explain concepts but weakly answer *where to act*.
2698
+
2699
+ ## Solution (three artifacts)
2700
+
2701
+ | Artifact | Role |
2702
+ |----------|------|
2703
+ | **AgentHandoff** | JSON handoff: \`startHere\`, \`editRoots\`, \`checks\`, \`humanDoc\`, \`bridge\` |
2704
+ | **DocBridgeIndex** | Deterministic index + \`contentHash\` for CI freshness gates |
2705
+ | **Self-describe** | \`llms.txt\`, \`capabilities.json\` for discovery |
2706
+
2707
+ ## Four loops
2708
+
2709
+ | Loop | Command | Outcome |
2710
+ |------|---------|---------|
2711
+ | **Act** | \`ak-docs query package <id> --agent\` | Agent edits only \`editRoots\` |
2712
+ | **Bridge** | \`ak-docs bootstrap agent-docs\` | Link agent corpus \u2194 human site |
2713
+ | **Learn** | \`ak-docs memory promote --pr\` | HITL draft PR from agent memory |
2714
+ | **Explain** | \`ak-docs ask "<question>"\` | Local consult + handoff preview |
2715
+
2716
+ ## 60-second proof
2717
+
2718
+ \`\`\`bash
2719
+ npm i -D @agentskit/doc-bridge
2720
+ npx ak-docs demo --text
2721
+ ak-docs mcp install --cursor
2722
+ \`\`\`
2723
+
2724
+ ## AgentHandoff example
2725
+
2726
+ \`\`\`json
2727
+ {
2728
+ "type": "agent-handoff",
2729
+ "startHere": "docs/for-agents/packages/auth.md",
2730
+ "editRoots": ["packages/auth"],
2731
+ "checks": ["pnpm --filter @demo/auth test"],
2732
+ "humanDoc": "/docs/guides/auth",
2733
+ "bridge": { "humanDoc": "linked" }
2734
+ }
2735
+ \`\`\`
2736
+
2737
+ When \`humanDoc\` is missing, handoffs surface \`bridge.action: "ak-docs bootstrap agent-docs"\` \u2014 a feature, not a silent gap.
2738
+
2739
+ ## MCP contract
2740
+
2741
+ Agents call \`handoff.resolve\` before editing \`packages/*\`:
2742
+
2743
+ 1. Read \`startHere\`
2744
+ 2. Stay inside \`editRoots\`
2745
+ 3. Run every \`checks\` command
2746
+ 4. Link \`humanDoc\` or escalate missing bridge
2747
+
2748
+ ## CI gate
2749
+
2750
+ \`\`\`yaml
2751
+ - uses: AgentsKit-io/doc-bridge@v1.0.0
2752
+ \`\`\`
2753
+
2754
+ Or: \`ak-docs index && ak-docs gate run\` \u2014 stale index fails the PR.
2755
+
2756
+ ## Coverage metric
2757
+
2758
+ \`\`\`bash
2759
+ ak-docs doctor --text
2760
+ ak-docs doctor --badge
2761
+ \`\`\`
2762
+
2763
+ Teams track handoff % and human-bridge % daily.
2764
+
2765
+ ## When to use
2766
+
2767
+ - pnpm/npm monorepos with real package ownership
2768
+ - Fumadocus / Docusaurus human sites + dense agent corpus
2769
+ - Cursor / Claude / Codex agents that should not guess edit roots
2770
+
2771
+ ## When not to use
2772
+
2773
+ - Single-file repos with no ownership boundaries (AGENTS.md may suffice)
2774
+ - Hosted doc chat as primary product (doc-bridge is routing + bridge, not SaaS chat)
2775
+
2776
+ ## References
2777
+
2778
+ - npm: https://www.npmjs.com/package/@agentskit/doc-bridge
2779
+ - repo: https://github.com/AgentsKit-io/doc-bridge
2780
+ - skill: https://github.com/AgentsKit-io/doc-bridge/blob/master/docs/skills/doc-bridge.md
2781
+ - landing: https://agentskit-io.github.io/doc-bridge/
2782
+ `;
2783
+ var docBridgePatternPayload = () => ({
2784
+ ...DOC_BRIDGE_PATTERN_META,
2785
+ format: "okf-pattern-v1",
2786
+ body: docBridgePatternMarkdown()
2787
+ });
2788
+
2789
+ // src/cli/demo.ts
2790
+ import { cpSync, existsSync as existsSync13, mkdtempSync, readFileSync as readFileSync15, rmSync } from "fs";
2791
+ import { tmpdir } from "os";
2792
+ import { dirname as dirname6, join as join18, resolve as resolve7 } from "path";
2793
+ import { fileURLToPath } from "url";
2794
+
2795
+ // src/mcp/install.ts
2796
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync14, writeFileSync as writeFileSync3 } from "fs";
2797
+ import { homedir } from "os";
2798
+ import { dirname as dirname5, join as join17, resolve as resolve6 } from "path";
2799
+ var SERVER_NAME = "ak-docs";
2800
+ var mcpServerEntry = (root) => ({
2801
+ command: "npx",
2802
+ args: ["ak-docs", "mcp"],
2803
+ cwd: root
2804
+ });
2805
+ var readJson = (path) => {
2806
+ if (!existsSync12(path)) return {};
2807
+ try {
2808
+ const parsed = JSON.parse(readFileSync14(path, "utf8"));
2809
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2810
+ } catch {
2811
+ return {};
2812
+ }
2813
+ };
2814
+ var writeJson = (path, value) => {
2815
+ mkdirSync3(dirname5(path), { recursive: true });
2816
+ writeFileSync3(path, `${JSON.stringify(value, null, 2)}
2817
+ `, "utf8");
2818
+ };
2819
+ var resolveTargetPath = (target, root) => {
2820
+ if (target === "cursor") return resolve6(root, ".cursor", "mcp.json");
2821
+ return join17(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
2822
+ };
2823
+ var installMcpConfig = (root, target) => {
2824
+ const configPath = resolveTargetPath(target, root);
2825
+ const created = !existsSync12(configPath);
2826
+ const existing = readJson(configPath);
2827
+ const servers = existing.mcpServers && typeof existing.mcpServers === "object" && !Array.isArray(existing.mcpServers) ? { ...existing.mcpServers } : {};
2828
+ servers[SERVER_NAME] = mcpServerEntry(root);
2829
+ writeJson(configPath, { ...existing, mcpServers: servers });
2830
+ const nextSteps = target === "cursor" ? [
2831
+ "Restart Cursor or reload MCP servers",
2832
+ "Paste the doc-bridge skill into Cursor rules (see docs/skills/doc-bridge.md)",
2833
+ "Before editing packages/* call handoff.resolve"
2834
+ ] : [
2835
+ "Restart Claude Desktop",
2836
+ "Run ak-docs index after doc changes"
2837
+ ];
2838
+ return {
2839
+ ok: true,
2840
+ target,
2841
+ configPath,
2842
+ created,
2843
+ serverName: SERVER_NAME,
2844
+ nextSteps
2845
+ };
2846
+ };
2847
+ var mcpSnippet = (root) => JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpServerEntry(root) } }, null, 2);
2848
+
2849
+ // src/cli/demo.ts
2850
+ var packageRoot = resolve7(dirname6(fileURLToPath(import.meta.url)), "..", "..");
2851
+ var fixturePath = (fixture) => {
2852
+ if (fixture === "monorepo") {
2853
+ return join18(packageRoot, "examples", "demo-monorepo");
2854
+ }
2855
+ return join18(packageRoot, "examples", "demo-example");
2856
+ };
2857
+ var formatHandoffText = (handoff) => {
2858
+ const bridge = handoff.bridge?.humanDoc === "missing" ? `human guide: missing \u2192 ${handoff.bridge.action ?? "ak-docs bootstrap agent-docs"}` : handoff.humanDoc ? `human guide: ${handoff.humanDoc}` : "human guide: (none)";
2859
+ return [
2860
+ `target: ${handoff.target.id} (${handoff.target.path ?? "n/a"})`,
2861
+ `start: ${handoff.startHere}`,
2862
+ `edit: ${handoff.editRoots.join(", ")}`,
2863
+ `checks: ${handoff.checks.length ? handoff.checks.join(" \xB7 ") : "(none)"}`,
2864
+ bridge,
2865
+ ...handoff.notes.length ? [`notes: ${handoff.notes[0]}`] : []
2866
+ ];
2867
+ };
2868
+ var runDemo = (root, config, fixture = "example", options = {}) => {
2869
+ const targetPackage = fixture === "monorepo" ? "auth" : "example";
2870
+ const fixtureDir = fixturePath(fixture);
2871
+ if (options.copyFixture && existsSync13(fixtureDir)) {
2872
+ for (const rel of ["doc-bridge.config.json", "docs", "packages", "pnpm-workspace.yaml", "package.json"]) {
2873
+ const src = join18(fixtureDir, rel);
2874
+ if (!existsSync13(src)) continue;
2875
+ const dest = join18(root, rel);
2876
+ cpSync(src, dest, { recursive: true });
2877
+ }
2878
+ }
2879
+ const gateBeforeStale = runGates(root, config, ["index-freshness"]);
2880
+ const beforeGate = gateBeforeStale.results[0] ?? { ok: false, message: "index-freshness unavailable" };
2881
+ buildDocBridgeIndex({ root, config });
2882
+ const index = loadDocBridgeIndex(root, config);
2883
+ const handoff = runQuery(index, config, {
2884
+ kind: "package",
2885
+ id: targetPackage,
2886
+ agent: true
2887
+ });
2888
+ const gateAfter = runGates(root, config, ["index-freshness"]);
2889
+ const afterGate = gateAfter.results[0] ?? { ok: true, message: "Index is fresh" };
2890
+ return {
2891
+ ok: afterGate.ok,
2892
+ fixture,
2893
+ handoff,
2894
+ gateBefore: { ok: beforeGate.ok, message: beforeGate.message },
2895
+ gateAfter: { ok: afterGate.ok, message: afterGate.message },
2896
+ mcpSnippet: mcpSnippet(root),
2897
+ nextCommands: [
2898
+ `ak-docs query package ${targetPackage} --agent`,
2899
+ "ak-docs doctor --text",
2900
+ "ak-docs mcp install --cursor",
2901
+ 'ak-docs ask "where do I change auth?"'
2902
+ ]
2903
+ };
2904
+ };
2905
+ var formatDemoText = (result) => {
2906
+ const lines = [
2907
+ "ak-docs demo \u2014 AgentHandoff in 60s",
2908
+ "\u2550".repeat(44),
2909
+ "",
2910
+ "Before (agent guesses package)",
2911
+ ' \u2717 edits packages/billing when task mentions "auth"',
2912
+ " \u2717 runs repo-wide test instead of package checks",
2913
+ "",
2914
+ "After (handoff.resolve / query --agent)",
2915
+ ...formatHandoffText(result.handoff).map((line) => ` \u2713 ${line}`),
2916
+ "",
2917
+ `Gate: ${result.gateBefore.ok ? "green" : "red"} \u2192 ${result.gateAfter.ok ? "green" : "red"}`,
2918
+ ` before: ${result.gateBefore.message}`,
2919
+ ` after: ${result.gateAfter.message}`,
2920
+ "",
2921
+ "MCP snippet (.cursor/mcp.json)",
2922
+ ...result.mcpSnippet.split("\n").map((line) => ` ${line}`),
2923
+ "",
2924
+ "Next",
2925
+ ...result.nextCommands.map((cmd) => ` \u2192 ${cmd}`)
2926
+ ];
2927
+ return lines;
2928
+ };
2929
+ var withDemoWorkspace = (fixture, fn) => {
2930
+ const dir = mkdtempSync(join18(tmpdir(), "ak-docs-demo-"));
2931
+ try {
2932
+ const fixtureDir = fixturePath(fixture);
2933
+ if (!existsSync13(fixtureDir)) {
2934
+ throw new Error(`Demo fixture "${fixture}" not found at ${fixtureDir}`);
2935
+ }
2936
+ cpSync(fixtureDir, dir, { recursive: true });
2937
+ const config = JSON.parse(readFileSync15(join18(dir, "doc-bridge.config.json"), "utf8"));
2938
+ return fn(dir, config);
2939
+ } finally {
2940
+ rmSync(dir, { recursive: true, force: true });
2941
+ }
2942
+ };
2943
+
2944
+ // src/doctor/run-doctor.ts
2945
+ var gradeForScore = (score) => {
2946
+ if (score >= 90) return "A";
2947
+ if (score >= 75) return "B";
2948
+ if (score >= 60) return "C";
2949
+ if (score >= 40) return "D";
2950
+ return "F";
2951
+ };
2952
+ var agentDocPaths = (index) => {
2953
+ const paths = /* @__PURE__ */ new Set();
2954
+ for (const owner of Object.values(index.lookup?.ownership ?? {})) {
2955
+ if (owner.agentDoc) paths.add(owner.agentDoc);
2956
+ }
2957
+ for (const handoff of Object.values(index.handoffs ?? {})) {
2958
+ if (handoff.startHere) paths.add(handoff.startHere);
2959
+ }
2960
+ return paths;
2961
+ };
2962
+ var computeScore = (coverage) => {
2963
+ let score = 0;
2964
+ if (coverage.freshness.hasIndex) score += 15;
2965
+ if (coverage.freshness.ok) score += 15;
2966
+ const { total, withAgentDoc, withHumanDoc } = coverage.packages;
2967
+ if (total > 0) {
2968
+ score += Math.round(withAgentDoc / total * 35);
2969
+ score += Math.round(withHumanDoc / total * 20);
2970
+ } else if (coverage.agentDocs.indexed > 0) {
2971
+ score += 35;
2972
+ }
2973
+ if (coverage.gates.ok) score += 15;
2974
+ else {
2975
+ const passed = coverage.gates.results.filter((gate) => gate.ok).length;
2976
+ const totalGates = coverage.gates.results.length || 1;
2977
+ score += Math.round(passed / totalGates * 10);
2978
+ }
2979
+ return Math.min(100, Math.max(0, score));
2980
+ };
2981
+ var buildIssues = (coverage) => {
2982
+ const issues = [];
2983
+ if (!coverage.freshness.hasIndex) {
2984
+ issues.push({
2985
+ severity: "error",
2986
+ code: "index-missing",
2987
+ message: "No doc-bridge index found.",
2988
+ action: "ak-docs index"
2989
+ });
2990
+ } else if (!coverage.freshness.ok) {
2991
+ issues.push({
2992
+ severity: "error",
2993
+ code: "index-stale",
2994
+ message: coverage.freshness.message,
2995
+ action: "ak-docs index"
2996
+ });
2997
+ }
2998
+ for (const id of coverage.packages.missingAgentDoc) {
2999
+ issues.push({
3000
+ severity: "warn",
3001
+ code: "missing-agent-doc",
3002
+ message: `Package "${id}" has no dedicated agent doc.`,
3003
+ action: `ak-docs init --scaffold-workspaces # or edit docs/for-agents/packages/${id}.md`
3004
+ });
3005
+ }
3006
+ for (const id of coverage.packages.missingHumanDoc) {
3007
+ issues.push({
3008
+ severity: "info",
3009
+ code: "missing-human-doc",
3010
+ message: `Package "${id}" has no linked human guide.`,
3011
+ action: "ak-docs bootstrap agent-docs"
3012
+ });
3013
+ }
3014
+ for (const gate of coverage.gates.results.filter((result) => !result.ok)) {
3015
+ issues.push({
3016
+ severity: gate.id === "index-freshness" ? "error" : "warn",
3017
+ code: `gate-${gate.id}`,
3018
+ message: gate.message,
3019
+ action: gate.id === "index-freshness" ? "ak-docs index" : "ak-docs gate run"
3020
+ });
3021
+ }
3022
+ return issues;
3023
+ };
3024
+ var buildNextActions = (issues, coverage) => {
3025
+ const actions = /* @__PURE__ */ new Set();
3026
+ for (const issue of issues) {
3027
+ if (issue.action) actions.add(issue.action);
3028
+ }
3029
+ if (!coverage.freshness.hasIndex || !coverage.freshness.ok) {
3030
+ actions.add("ak-docs index");
3031
+ }
3032
+ if (coverage.packages.missingHumanDoc.length) {
3033
+ actions.add("ak-docs bootstrap agent-docs");
3034
+ }
3035
+ if (coverage.packages.total > 0) {
3036
+ const sample = coverage.packages.missingAgentDoc[0] ?? coverage.packages.missingHumanDoc[0];
3037
+ if (sample) actions.add(`ak-docs query package ${sample} --agent`);
3038
+ }
3039
+ if (!actions.size) {
3040
+ actions.add("ak-docs mcp install --cursor");
3041
+ actions.add("ak-docs gate run");
3042
+ }
3043
+ return [...actions].slice(0, 6);
3044
+ };
3045
+ var runDoctor = (root, config) => {
3046
+ let index;
3047
+ let hasIndex = true;
3048
+ let freshnessOk = false;
3049
+ let freshnessMessage = "Index is fresh";
3050
+ try {
3051
+ index = loadDocBridgeIndex(root, config);
3052
+ const next = buildDocBridgeIndex({ root, config, write: false }).index.contentHash;
3053
+ freshnessOk = index.contentHash === next;
3054
+ freshnessMessage = freshnessOk ? "Index is fresh" : "Index is stale. Run: ak-docs index";
3055
+ } catch (error) {
3056
+ if (error instanceof IndexNotFoundError) {
3057
+ hasIndex = false;
3058
+ freshnessOk = false;
3059
+ freshnessMessage = error.message;
3060
+ index = buildDocBridgeIndex({ root, config, write: false }).index;
3061
+ } else {
3062
+ throw error;
3063
+ }
3064
+ }
3065
+ const ownership = Object.entries(index.lookup?.ownership ?? {});
3066
+ const missingAgentDoc = ownership.filter(([, owner]) => !owner.agentDoc || owner.agentDoc === config.corpus.agent.index).map(([id]) => id);
3067
+ const missingHumanDoc = ownership.filter(([, owner]) => !owner.humanDoc).map(([id]) => id);
3068
+ const indexedPaths = new Set(index.knowledge.map((entry) => entry.path));
3069
+ const expectedAgentDocs = agentDocPaths(index);
3070
+ const unindexed = [...expectedAgentDocs].filter((path) => !indexedPaths.has(path));
3071
+ const corpusDocs = scanAgentCorpus(root, config).filter(
3072
+ (doc) => doc.path !== config.corpus.agent.index
3073
+ );
3074
+ const gates = runGates(root, config);
3075
+ const coverage = {
3076
+ packages: {
3077
+ total: ownership.length,
3078
+ withAgentDoc: ownership.length - missingAgentDoc.length,
3079
+ withHumanDoc: ownership.length - missingHumanDoc.length,
3080
+ missingAgentDoc,
3081
+ missingHumanDoc
3082
+ },
3083
+ agentDocs: {
3084
+ total: corpusDocs.length,
3085
+ indexed: corpusDocs.filter((doc) => indexedPaths.has(doc.path)).length,
3086
+ unindexed: corpusDocs.filter((doc) => !indexedPaths.has(doc.path)).map((doc) => doc.path)
3087
+ },
3088
+ freshness: {
3089
+ ok: freshnessOk,
3090
+ message: freshnessMessage,
3091
+ hasIndex
3092
+ },
3093
+ gates
3094
+ };
3095
+ const issues = buildIssues(coverage);
3096
+ const score = computeScore(coverage);
3097
+ const nextActions = buildNextActions(issues, coverage);
3098
+ const report = {
3099
+ ok: issues.every((issue) => issue.severity !== "error") && gates.ok,
3100
+ score,
3101
+ grade: gradeForScore(score),
3102
+ coverage,
3103
+ badge: { handoffPct: 0, bridgePct: 0, score, grade: gradeForScore(score), packages: 0 },
3104
+ issues,
3105
+ nextActions
3106
+ };
3107
+ return { ...report, badge: doctorBadgeMetrics(report) };
3108
+ };
3109
+ var formatDoctorText = (report) => {
3110
+ const { coverage } = report;
3111
+ const handoffPct = coverage.packages.total > 0 ? Math.round(coverage.packages.withAgentDoc / coverage.packages.total * 100) : 0;
3112
+ const humanPct = coverage.packages.total > 0 ? Math.round(coverage.packages.withHumanDoc / coverage.packages.total * 100) : 0;
3113
+ const lines = [
3114
+ "doc-bridge doctor",
3115
+ "\u2500".repeat(40),
3116
+ `Score: ${report.score}/100 (${report.grade})`,
3117
+ "",
3118
+ "Coverage",
3119
+ ` Packages: ${coverage.packages.total}`,
3120
+ ` Agent docs: ${coverage.packages.withAgentDoc}/${coverage.packages.total} (${handoffPct}% handoff-ready)`,
3121
+ ` Human guides: ${coverage.packages.withHumanDoc}/${coverage.packages.total} (${humanPct}% bridged)`,
3122
+ ` Corpus indexed: ${coverage.agentDocs.indexed}/${coverage.agentDocs.total} agent docs`,
3123
+ ` Index freshness: ${coverage.freshness.ok ? "fresh" : "stale or missing"}`,
3124
+ ` Gates: ${coverage.gates.results.filter((g) => g.ok).length}/${coverage.gates.results.length} passing`,
3125
+ ` Badge: handoff ${report.badge.handoffPct}% \xB7 bridge ${report.badge.bridgePct}%`
3126
+ ];
3127
+ if (coverage.packages.missingHumanDoc.length) {
3128
+ lines.push("", "Missing humanDoc (bridge gap)", ...coverage.packages.missingHumanDoc.map((id) => ` \u2022 ${id}`));
3129
+ }
3130
+ if (coverage.packages.missingAgentDoc.length) {
3131
+ lines.push("", "Missing agent doc", ...coverage.packages.missingAgentDoc.map((id) => ` \u2022 ${id}`));
3132
+ }
3133
+ if (report.issues.length) {
3134
+ lines.push("", "Issues");
3135
+ for (const issue of report.issues.slice(0, 8)) {
3136
+ const icon = issue.severity === "error" ? "\u2717" : issue.severity === "warn" ? "!" : "\xB7";
3137
+ lines.push(` ${icon} [${issue.code}] ${issue.message}`);
3138
+ }
3139
+ if (report.issues.length > 8) {
3140
+ lines.push(` \u2026 +${report.issues.length - 8} more`);
3141
+ }
3142
+ }
3143
+ lines.push("", "Next actions", ...report.nextActions.map((action) => ` \u2192 ${action}`));
3144
+ return lines;
3145
+ };
3146
+
2293
3147
  // src/mcp/server.ts
2294
- import { readFileSync as readFileSync14, realpathSync } from "fs";
2295
- import { relative, resolve as resolve5 } from "path";
3148
+ import { readFileSync as readFileSync16, realpathSync } from "fs";
3149
+ import { relative, resolve as resolve8 } from "path";
2296
3150
  import { z as z5, ZodError } from "zod";
2297
3151
  var MCP_TOOLS = [
2298
3152
  {
@@ -2399,7 +3253,7 @@ var findDocPath = (index, args) => {
2399
3253
  };
2400
3254
  var resolveDocPath = (root, relPath) => {
2401
3255
  const rootAbs = realpathSync.native(root);
2402
- const unresolved = resolve5(rootAbs, relPath);
3256
+ const unresolved = resolve8(rootAbs, relPath);
2403
3257
  const unresolvedRel = relative(rootAbs, unresolved);
2404
3258
  if (unresolvedRel.startsWith("..")) throw new Error("doc.get path escapes project root");
2405
3259
  const abs = realpathSync.native(unresolved);
@@ -2437,7 +3291,7 @@ var handleMcpRequest = (ctx, request) => {
2437
3291
  }
2438
3292
  if (name === "doc.get") {
2439
3293
  const relPath = findDocPath(index(), parseToolArgs("doc.get", DocGetArgsSchema, args));
2440
- return textResult(readFileSync14(resolveDocPath(ctx.root, relPath), "utf8"));
3294
+ return textResult(readFileSync16(resolveDocPath(ctx.root, relPath), "utf8"));
2441
3295
  }
2442
3296
  if (name === "gate.status") return textResult(runGates(ctx.root, ctx.config));
2443
3297
  if (name === "retriever.query") {
@@ -2519,14 +3373,17 @@ var usage = `ak-docs \u2014 human\u2194agent documentation bridge (@agentskit/do
2519
3373
 
2520
3374
  Core (no API key):
2521
3375
  ak-docs init [--demo] [--scaffold-workspaces]
2522
- ak-docs index
3376
+ ak-docs demo [--fixture example|monorepo] [--text] [--in-project]
3377
+ ak-docs doctor [--text] [--badge] [--write-badge]
3378
+ ak-docs index [--watch]
2523
3379
  ak-docs query <package|ownership|intent|change> <id> [--agent] [--text]
2524
3380
  ak-docs search <term> [--agent] [--text]
2525
3381
  ak-docs list <packages|intents|changes|knowledge> [--text]
2526
3382
  ak-docs ask [question] local consult (no LLM)
2527
3383
  ak-docs gate run [gate-id]
2528
3384
  ak-docs mcp
2529
- ak-docs memory ingest|classify|promote
3385
+ ak-docs mcp install --cursor | --claude
3386
+ ak-docs memory ingest|classify|promote [--pr] [--dry-run]
2530
3387
  ak-docs bootstrap agent-docs
2531
3388
  ak-docs validate-config | validate-handoff <file>
2532
3389
 
@@ -2538,7 +3395,7 @@ Intelligence (optional AgentsKit peers):
2538
3395
  Advanced / ecosystem:
2539
3396
  ak-docs retrieve <query>
2540
3397
  ak-docs registry topology
2541
- ak-docs playbook draft
3398
+ ak-docs playbook draft | pattern [--text]
2542
3399
 
2543
3400
  Global flags:
2544
3401
  -h, --help --version
@@ -2579,6 +3436,8 @@ var parseArgs = (argv) => {
2579
3436
  else if (positional[0] === "index") command = "index";
2580
3437
  else if (positional[0] === "gate") command = "gate";
2581
3438
  else if (positional[0] === "mcp") command = "mcp";
3439
+ else if (positional[0] === "doctor") command = "doctor";
3440
+ else if (positional[0] === "demo") command = "demo";
2582
3441
  else if (positional[0] === "query") command = "query";
2583
3442
  else if (positional[0] === "search") command = "search";
2584
3443
  else if (positional[0] === "retrieve") command = "retrieve";
@@ -2588,7 +3447,7 @@ var parseArgs = (argv) => {
2588
3447
  else if (positional[0] === "list") command = "list";
2589
3448
  return { command, flags, configPath, positional };
2590
3449
  };
2591
- var writeJson = (payload) => {
3450
+ var writeJson2 = (payload) => {
2592
3451
  process.stdout.write(`${JSON.stringify(payload, null, 2)}
2593
3452
  `);
2594
3453
  };
@@ -2614,53 +3473,77 @@ var writeTextQuery = (payload) => {
2614
3473
  writeLines([
2615
3474
  `Search: ${String(data.term ?? "")}`,
2616
3475
  `Matches: ${String(data.count ?? matches.length)}`,
2617
- ...matches.map(
2618
- (match) => [match.type, match.id, match.path, match.summary].filter(Boolean).join(" ")
2619
- )
3476
+ ...matches.length ? matches.map(
3477
+ (match) => formatSearchMatch(match)
3478
+ ) : [" (none)"]
2620
3479
  ]);
2621
3480
  return;
2622
3481
  }
2623
3482
  writeLines([textValue(result.data)]);
2624
3483
  };
3484
+ var formatSearchMatch = (match) => {
3485
+ const summary = match.summary ? match.summary.replace(/\s+/g, " ").slice(0, 100) : "";
3486
+ const score = typeof match.score === "number" ? ` score=${match.score}` : "";
3487
+ return ` [${match.type}] ${match.id}${score}
3488
+ ${match.path}${summary ? `
3489
+ ${summary}` : ""}`;
3490
+ };
2625
3491
  var writeTextSearch = (term, matches) => {
2626
3492
  writeLines([
2627
3493
  `Search: ${term}`,
2628
3494
  `Matches: ${matches.length}`,
2629
- ...matches.map(
2630
- (match) => [match.type, match.id, match.path, match.summary].filter(Boolean).join(" ")
2631
- )
3495
+ ...matches.length ? matches.map(formatSearchMatch) : [" (none)"]
2632
3496
  ]);
2633
3497
  };
2634
- var writeAsk = (question, matches, index) => {
2635
- const best = matches[0];
2636
- const owner = matches.find((match) => match.type === "ownership" || index.lookup?.ownership?.[match.id]);
2637
- const bestQuery = owner ? `ak-docs query ownership ${owner.id} --agent` : "ak-docs list knowledge --text";
3498
+ var handoffSummaryLines = (index, config, ownerId) => {
3499
+ try {
3500
+ const handoff = runQuery(index, config, { kind: "ownership", id: ownerId, agent: true });
3501
+ if (!handoff || typeof handoff !== "object" || !("editRoots" in handoff)) return [];
3502
+ const payload = handoff;
3503
+ const bridgeLine = payload.bridge?.humanDoc === "missing" ? `Bridge: human guide missing \u2192 ${payload.bridge.action ?? "ak-docs bootstrap agent-docs"}` : payload.humanDoc ? `Bridge: ${payload.humanDoc}` : "Bridge: (no human plugin configured)";
3504
+ return [
3505
+ "",
3506
+ "Handoff preview",
3507
+ ` start: ${payload.startHere ?? "(unknown)"}`,
3508
+ ` edit: ${(payload.editRoots ?? []).join(", ") || "(none)"}`,
3509
+ ` checks: ${(payload.checks ?? []).join(" \xB7 ") || "(none)"}`,
3510
+ ` ${bridgeLine}`
3511
+ ];
3512
+ } catch {
3513
+ return [];
3514
+ }
3515
+ };
3516
+ var writeAsk = (question, matches, index, config) => {
3517
+ const owner = matches.find((match) => match.type === "ownership") ?? matches.find((match) => Boolean(index.lookup?.ownership?.[match.id]));
3518
+ const best = owner ?? matches[0];
3519
+ const ownerId = best && (best.type === "ownership" || index.lookup?.ownership?.[best.id]) ? best.id : void 0;
3520
+ const bestQuery = ownerId ? `ak-docs query ownership ${ownerId} --agent` : "ak-docs list knowledge --text";
2638
3521
  writeLines([
2639
3522
  `Question: ${question}`,
2640
3523
  best ? `Best match: ${best.type} ${best.id} (${best.path})` : "Best match: none",
3524
+ ...ownerId ? handoffSummaryLines(index, config, ownerId) : [],
2641
3525
  "",
2642
3526
  "Matches:",
2643
- ...matches.length ? matches.slice(0, 5).map(
2644
- (match) => [match.type, match.id, match.path, match.summary].filter(Boolean).join(" ")
2645
- ) : ["No local matches. Try: ak-docs search <term>"],
3527
+ ...matches.length ? matches.slice(0, 5).map(formatSearchMatch) : [" No local matches. Try: ak-docs search <term>"],
2646
3528
  "",
2647
3529
  "Next commands:",
2648
3530
  ...best ? [
2649
3531
  `ak-docs search "${question}" --agent`,
2650
- bestQuery
2651
- ] : ["ak-docs list knowledge --text"]
3532
+ bestQuery,
3533
+ ...ownerId ? [`ak-docs doctor --text`] : []
3534
+ ] : ["ak-docs list knowledge --text", "ak-docs doctor --text"]
2652
3535
  ]);
2653
3536
  };
2654
3537
  var readIndexedDoc = (root, config, idOrPath) => {
2655
3538
  const index = loadDocBridgeIndex(root, config);
2656
3539
  const entry = index.knowledge.find((doc) => doc.id === idOrPath || doc.path === idOrPath);
2657
3540
  if (!entry) throw new Error(`Unknown indexed doc "${idOrPath}". Try: search ${idOrPath}`);
2658
- const abs = resolve6(root, entry.path);
2659
- const rootAbs = resolve6(root);
3541
+ const abs = resolve9(root, entry.path);
3542
+ const rootAbs = resolve9(root);
2660
3543
  if (abs !== rootAbs && !abs.startsWith(`${rootAbs}/`)) {
2661
3544
  throw new Error(`Indexed doc escapes project root: ${entry.path}`);
2662
3545
  }
2663
- return readFileSync15(abs, "utf8");
3546
+ return readFileSync17(abs, "utf8");
2664
3547
  };
2665
3548
  var runAskRepl = async (root, config) => {
2666
3549
  const index = loadDocBridgeIndex(root, config);
@@ -2679,12 +3562,12 @@ var runAskRepl = async (root, config) => {
2679
3562
  process.stdout.write(`${readIndexedDoc(root, config, value).slice(0, 8e3)}
2680
3563
  `);
2681
3564
  } else if (command === "resolve") {
2682
- writeJson(runQuery(index, config, { kind: "ownership", id: value, agent: true }));
3565
+ writeJson2(runQuery(index, config, { kind: "ownership", id: value, agent: true }));
2683
3566
  } else if (command === "gate") {
2684
3567
  const gateId = value || void 0;
2685
- writeJson(runGates(root, config, gateId ? [gateId] : void 0));
3568
+ writeJson2(runGates(root, config, gateId ? [gateId] : void 0));
2686
3569
  } else {
2687
- writeAsk(line, searchIndex(index, line, 8), index);
3570
+ writeAsk(line, searchIndex(index, line, 8), index, config);
2688
3571
  }
2689
3572
  } catch (error) {
2690
3573
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -2736,9 +3619,9 @@ var diagnosticNextCommands = (config, result) => {
2736
3619
  ];
2737
3620
  };
2738
3621
  var writeIfMissing = (path, contents) => {
2739
- if (existsSync10(path)) return false;
2740
- mkdirSync2(dirname4(path), { recursive: true });
2741
- writeFileSync2(path, contents, "utf8");
3622
+ if (existsSync14(path)) return false;
3623
+ mkdirSync5(dirname7(path), { recursive: true });
3624
+ writeFileSync4(path, contents, "utf8");
2742
3625
  return true;
2743
3626
  };
2744
3627
  var demoOwnership = {
@@ -2841,7 +3724,7 @@ var scaffoldWorkspaceDocs = (root, config) => {
2841
3724
  const created = [];
2842
3725
  const skipped = [];
2843
3726
  for (const pkg of discoverPnpmPackages(root, config)) {
2844
- const path = resolve6(root, config.corpus.agent.root, "packages", `${pkg.id}.md`);
3727
+ const path = resolve9(root, config.corpus.agent.root, "packages", `${pkg.id}.md`);
2845
3728
  if (writeIfMissing(path, workspaceDocDraft(pkg.id, pkg.path))) created.push(path);
2846
3729
  else skipped.push(path);
2847
3730
  }
@@ -2851,11 +3734,11 @@ var bootstrapAgentDocs = (root, config) => {
2851
3734
  const created = [];
2852
3735
  const skipped = [];
2853
3736
  for (const doc of scanHumanDocRecords(root, config)) {
2854
- const raw = readFileSync15(doc.path, "utf8");
3737
+ const raw = readFileSync17(doc.path, "utf8");
2855
3738
  const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, "");
2856
3739
  const title = firstHeading(body) ?? doc.id;
2857
3740
  const description = firstParagraph(body);
2858
- const draftPath = resolve6(root, config.corpus.agent.root, "human", `${doc.id}.md`);
3741
+ const draftPath = resolve9(root, config.corpus.agent.root, "human", `${doc.id}.md`);
2859
3742
  const draft = [
2860
3743
  "---",
2861
3744
  "type: knowledge",
@@ -2904,7 +3787,7 @@ var runCli = (argv) => {
2904
3787
  configPath ? { explicitPath: configPath } : {}
2905
3788
  );
2906
3789
  parseDocBridgeConfig(config);
2907
- writeJson({ ok: true, path, schemaVersion: config.schemaVersion });
3790
+ writeJson2({ ok: true, path, schemaVersion: config.schemaVersion });
2908
3791
  return 0;
2909
3792
  } catch (error) {
2910
3793
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -2919,10 +3802,10 @@ var runCli = (argv) => {
2919
3802
  return 1;
2920
3803
  }
2921
3804
  try {
2922
- const abs = resolve6(file);
2923
- const raw = readFileSync15(abs, "utf8");
3805
+ const abs = resolve9(file);
3806
+ const raw = readFileSync17(abs, "utf8");
2924
3807
  const handoff = parseAgentHandoff(JSON.parse(raw));
2925
- writeJson({ ok: true, handoff });
3808
+ writeJson2({ ok: true, handoff });
2926
3809
  return 0;
2927
3810
  } catch (error) {
2928
3811
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -2933,10 +3816,10 @@ var runCli = (argv) => {
2933
3816
  if (command === "init") {
2934
3817
  const root = process.cwd();
2935
3818
  const withDemo = flags.has("--demo") || !flags.has("--no-demo");
2936
- const configFile = resolve6(root, configPath ?? "doc-bridge.config.json");
2937
- const docsIndex = resolve6(root, "docs/for-agents/INDEX.md");
2938
- const exampleDoc = resolve6(root, "docs/for-agents/packages/example.md");
2939
- const agentsMd = resolve6(root, "AGENTS.md");
3819
+ const configFile = resolve9(root, configPath ?? "doc-bridge.config.json");
3820
+ const docsIndex = resolve9(root, "docs/for-agents/INDEX.md");
3821
+ const exampleDoc = resolve9(root, "docs/for-agents/packages/example.md");
3822
+ const agentsMd = resolve9(root, "AGENTS.md");
2940
3823
  const configWritten = writeIfMissing(configFile, initConfigContents(configFile, withDemo));
2941
3824
  const indexWritten = writeIfMissing(
2942
3825
  docsIndex,
@@ -2944,9 +3827,9 @@ var runCli = (argv) => {
2944
3827
  );
2945
3828
  const exampleWritten = withDemo ? writeIfMissing(exampleDoc, exampleAgentDoc) : false;
2946
3829
  const agentsWritten = writeIfMissing(agentsMd, agentsMdSnippet);
2947
- if (withDemo) writeIfMissing(resolve6(root, "src/.gitkeep"), "");
3830
+ if (withDemo) writeIfMissing(resolve9(root, "src/.gitkeep"), "");
2948
3831
  const scaffold = flags.has("--scaffold-workspaces") ? scaffoldWorkspaceDocs(root, loadProject(configFile).config) : void 0;
2949
- writeJson({
3832
+ writeJson2({
2950
3833
  ok: true,
2951
3834
  configPath: configFile,
2952
3835
  demo: withDemo,
@@ -2970,7 +3853,7 @@ var runCli = (argv) => {
2970
3853
  try {
2971
3854
  const { config, root } = loadProject(configPath);
2972
3855
  const result = bootstrapAgentDocs(root, config);
2973
- writeJson({ ok: true, ...result });
3856
+ writeJson2({ ok: true, ...result });
2974
3857
  return 0;
2975
3858
  } catch (error) {
2976
3859
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -2980,24 +3863,43 @@ var runCli = (argv) => {
2980
3863
  }
2981
3864
  if (command === "memory") {
2982
3865
  if (!["ingest", "classify", "promote"].includes(positional[1] ?? "")) {
2983
- process.stderr.write("Usage: ak-docs memory <ingest|classify|promote> [--config <path>]\n");
3866
+ process.stderr.write(
3867
+ "Usage: ak-docs memory <ingest|classify|promote> [--pr] [--dry-run] [--force] [--config <path>]\n"
3868
+ );
2984
3869
  return 1;
2985
3870
  }
2986
3871
  try {
2987
3872
  const { config, root } = loadProject(configPath);
2988
3873
  const candidates = ingestMemoryCandidates(root);
2989
3874
  if (positional[1] === "ingest") {
2990
- writeJson({ ok: true, count: candidates.length, candidates });
3875
+ writeJson2({ ok: true, count: candidates.length, candidates });
2991
3876
  return 0;
2992
3877
  }
2993
3878
  const index = loadDocBridgeIndex(root, config);
2994
3879
  const classifications = classifyMemoryCandidates(candidates, index);
2995
3880
  if (positional[1] === "classify") {
2996
- writeJson({ ok: true, count: classifications.length, classifications });
3881
+ writeJson2({ ok: true, count: classifications.length, classifications });
2997
3882
  return 0;
2998
3883
  }
2999
3884
  const draft = draftMemoryPromotion(classifications);
3000
- writeJson(draft);
3885
+ if (flags.has("--pr") || flags.has("--github")) {
3886
+ const pr = promoteMemoryToGithubPr(root, draft, {
3887
+ dryRun: flags.has("--dry-run"),
3888
+ force: flags.has("--force")
3889
+ });
3890
+ if (flags.has("--text")) {
3891
+ writeLines([
3892
+ pr.message,
3893
+ ...pr.draftPath ? [`Draft: ${pr.draftPath}`] : [],
3894
+ ...pr.prUrl ? [`PR: ${pr.prUrl}`] : [],
3895
+ ...pr.commands.length ? ["", "Commands:", ...pr.commands.map((cmd) => ` ${cmd}`)] : []
3896
+ ]);
3897
+ } else {
3898
+ writeJson2({ ...draft, pr });
3899
+ }
3900
+ return pr.ok ? 0 : 1;
3901
+ }
3902
+ writeJson2(draft);
3001
3903
  return draft.ok ? 0 : 1;
3002
3904
  } catch (error) {
3003
3905
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -3010,22 +3912,35 @@ var runCli = (argv) => {
3010
3912
  process.stderr.write("Usage: ak-docs registry topology\n");
3011
3913
  return 1;
3012
3914
  }
3013
- writeJson(registryTopology());
3915
+ writeJson2(registryTopology());
3014
3916
  return 0;
3015
3917
  }
3016
3918
  if (command === "playbook") {
3017
- if (positional[1] !== "draft") {
3018
- process.stderr.write("Usage: ak-docs playbook draft [--config <path>]\n");
3919
+ const action = positional[1];
3920
+ if (action === "pattern") {
3921
+ const payload = docBridgePatternPayload();
3922
+ if (flags.has("--text") || wantsTextOutput(flags, { schemaVersion: 1, corpus: { agent: { root: "docs" } } })) {
3923
+ process.stdout.write(`${docBridgePatternMarkdown()}
3924
+ `);
3925
+ } else {
3926
+ writeJson2(payload);
3927
+ }
3928
+ return 0;
3929
+ }
3930
+ if (action !== "draft") {
3931
+ process.stderr.write("Usage: ak-docs playbook draft | pattern [--text] [--config <path>]\n");
3019
3932
  return 1;
3020
3933
  }
3021
3934
  try {
3022
3935
  const { config, root } = loadProject(configPath);
3023
3936
  const index = loadDocBridgeIndex(root, config);
3024
3937
  const draft = draftMemoryPromotion(classifyMemoryCandidates(ingestMemoryCandidates(root), index));
3025
- writeJson({
3938
+ writeJson2({
3026
3939
  ...draft,
3027
3940
  title: "Draft Playbook feedback promotion",
3028
- pattern: "Doc Bridge Pattern"
3941
+ pattern: "Doc Bridge Pattern",
3942
+ patternDoc: "docs/playbook/doc-bridge-pattern.md",
3943
+ exportCommand: "ak-docs playbook pattern --text"
3029
3944
  });
3030
3945
  return 0;
3031
3946
  } catch (error) {
@@ -3036,11 +3951,18 @@ var runCli = (argv) => {
3036
3951
  }
3037
3952
  if (command === "index") {
3038
3953
  try {
3039
- const { config, root } = loadProject(configPath);
3954
+ const { config, root, configPath: loadedConfigPath } = loadProject(configPath);
3955
+ if (flags.has("--watch")) {
3956
+ return watchDocBridgeIndex({
3957
+ root,
3958
+ config,
3959
+ configPath: loadedConfigPath
3960
+ });
3961
+ }
3040
3962
  const result = buildDocBridgeIndex({ root, config });
3041
3963
  const diagnostics = indexDiagnostics(config, result);
3042
3964
  const handoffCount = Object.keys(result.index.handoffs ?? {}).length;
3043
- writeJson({
3965
+ writeJson2({
3044
3966
  ok: true,
3045
3967
  indexPath: result.indexPath,
3046
3968
  ...result.llmsTxtPath ? { llmsTxtPath: result.llmsTxtPath } : {},
@@ -3076,7 +3998,7 @@ var runCli = (argv) => {
3076
3998
  try {
3077
3999
  const { config, root } = loadProject(configPath);
3078
4000
  const result = runGates(root, config, gateId ? [gateId] : void 0);
3079
- writeJson(result);
4001
+ writeJson2(result);
3080
4002
  return result.ok ? 0 : 1;
3081
4003
  } catch (error) {
3082
4004
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -3085,12 +4007,98 @@ var runCli = (argv) => {
3085
4007
  }
3086
4008
  }
3087
4009
  if (command === "mcp") {
4010
+ if (positional[1] === "install") {
4011
+ const target = flags.has("--claude") ? "claude" : flags.has("--cursor") ? "cursor" : void 0;
4012
+ if (!target) {
4013
+ process.stderr.write("Usage: ak-docs mcp install --cursor | --claude\n");
4014
+ return 1;
4015
+ }
4016
+ try {
4017
+ const { config, root } = loadProject(configPath);
4018
+ const result = installMcpConfig(root, target);
4019
+ if (wantsTextOutput(flags, config)) {
4020
+ writeLines([
4021
+ `Installed MCP server "${result.serverName}" for ${result.target}`,
4022
+ `Config: ${result.configPath}`,
4023
+ ...result.created ? ["Created new config file"] : ["Merged into existing config"],
4024
+ "",
4025
+ "Next steps:",
4026
+ ...result.nextSteps.map((step) => ` \u2192 ${step}`)
4027
+ ]);
4028
+ } else {
4029
+ writeJson2({ ...result, snippet: mcpSnippet(root) });
4030
+ }
4031
+ return 0;
4032
+ } catch (error) {
4033
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
4034
+ `);
4035
+ return 1;
4036
+ }
4037
+ }
3088
4038
  try {
3089
4039
  const { config, root } = loadProject(configPath);
3090
4040
  startMcpStdioServer({ root, config });
3091
4041
  return void 0;
3092
4042
  } catch (error) {
3093
4043
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
4044
+ `);
4045
+ return 1;
4046
+ }
4047
+ }
4048
+ if (command === "doctor") {
4049
+ try {
4050
+ const { config, root } = loadProject(configPath);
4051
+ const report = runDoctor(root, config);
4052
+ if (flags.has("--write-badge")) {
4053
+ const badgePath = resolve9(root, ".doc-bridge", "coverage-badge.json");
4054
+ mkdirSync5(dirname7(badgePath), { recursive: true });
4055
+ writeFileSync4(badgePath, `${formatDoctorBadgeJson(report.badge)}
4056
+ `, "utf8");
4057
+ }
4058
+ if (flags.has("--badge")) {
4059
+ writeLines([formatDoctorBadgeMarkdown(report.badge)]);
4060
+ return 0;
4061
+ }
4062
+ if (wantsTextOutput(flags, config)) writeLines(formatDoctorText(report));
4063
+ else writeJson2(report);
4064
+ return report.ok ? 0 : 1;
4065
+ } catch (error) {
4066
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
4067
+ `);
4068
+ return 1;
4069
+ }
4070
+ }
4071
+ if (command === "demo") {
4072
+ const resolveDemoFixture = () => {
4073
+ const fixtureFlagIndex = positional.indexOf("--fixture");
4074
+ if (fixtureFlagIndex >= 0) {
4075
+ const value = positional[fixtureFlagIndex + 1];
4076
+ if (value === "monorepo" || value === "example") return value;
4077
+ }
4078
+ if (flags.has("--monorepo") || positional.includes("monorepo")) return "monorepo";
4079
+ if (positional[1] === "monorepo") return "monorepo";
4080
+ return "example";
4081
+ };
4082
+ const resolvedFixture = resolveDemoFixture();
4083
+ try {
4084
+ const useProject = flags.has("--in-project") || flags.has("--copy-fixture");
4085
+ if (!useProject) {
4086
+ const result2 = withDemoWorkspace(
4087
+ resolvedFixture,
4088
+ (root2, config2) => runDemo(root2, config2, resolvedFixture)
4089
+ );
4090
+ const textMode = flags.has("--text") || !flags.has("--json") && !flags.has("--agent");
4091
+ if (textMode) writeLines(formatDemoText(result2));
4092
+ else writeJson2(result2);
4093
+ return result2.ok ? 0 : 1;
4094
+ }
4095
+ const { config, root } = loadProject(configPath);
4096
+ const result = runDemo(root, config, resolvedFixture, { copyFixture: flags.has("--copy-fixture") });
4097
+ if (wantsTextOutput(flags, config)) writeLines(formatDemoText(result));
4098
+ else writeJson2(result);
4099
+ return result.ok ? 0 : 1;
4100
+ } catch (error) {
4101
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}
3094
4102
  `);
3095
4103
  return 1;
3096
4104
  }
@@ -3112,7 +4120,7 @@ var runCli = (argv) => {
3112
4120
  const index = loadDocBridgeIndex(root, config);
3113
4121
  const result = runQuery(index, config, { kind, id, agent: flags.has("--agent") });
3114
4122
  if (wantsTextOutput(flags, config)) writeTextQuery(result);
3115
- else writeJson(result);
4123
+ else writeJson2(result);
3116
4124
  return 0;
3117
4125
  } catch (error) {
3118
4126
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -3131,11 +4139,11 @@ var runCli = (argv) => {
3131
4139
  const index = loadDocBridgeIndex(root, config);
3132
4140
  if (flags.has("--agent")) {
3133
4141
  const result = runQuery(index, config, { kind: "search", term, agent: true });
3134
- writeJson(result);
4142
+ writeJson2(result);
3135
4143
  } else {
3136
4144
  const matches = searchIndex(index, term);
3137
4145
  if (wantsTextOutput(flags, config)) writeTextSearch(term, matches);
3138
- else writeJson({ term, count: matches.length, matches });
4146
+ else writeJson2({ term, count: matches.length, matches });
3139
4147
  }
3140
4148
  return 0;
3141
4149
  } catch (error) {
@@ -3159,7 +4167,7 @@ var runCli = (argv) => {
3159
4167
  try {
3160
4168
  const { config, root } = loadProject(configPath);
3161
4169
  const index = loadDocBridgeIndex(root, config);
3162
- writeJson({ query, chunks: await retrieveHybridChunks(root, config, index, query) });
4170
+ writeJson2({ query, chunks: await retrieveHybridChunks(root, config, index, query) });
3163
4171
  return 0;
3164
4172
  } catch (error) {
3165
4173
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -3181,7 +4189,7 @@ var runCli = (argv) => {
3181
4189
  const rag = await createDocBridgeRag(root, config, index);
3182
4190
  if (action === "ingest") {
3183
4191
  const result = await rag.ingest();
3184
- writeJson({ ok: true, ...result });
4192
+ writeJson2({ ok: true, ...result });
3185
4193
  return 0;
3186
4194
  }
3187
4195
  const query = positional.slice(2).join(" ").trim();
@@ -3190,7 +4198,7 @@ var runCli = (argv) => {
3190
4198
  return 1;
3191
4199
  }
3192
4200
  const hits = await rag.search(query);
3193
- writeJson({ query, count: hits.length, hits });
4201
+ writeJson2({ query, count: hits.length, hits });
3194
4202
  return 0;
3195
4203
  } catch (error) {
3196
4204
  if (error instanceof PeerMissingError) {
@@ -3272,7 +4280,7 @@ Install peers: ${layer1InstallHint()}
3272
4280
  return 1;
3273
4281
  }
3274
4282
  const index = loadDocBridgeIndex(root, config);
3275
- writeAsk(question, searchIndex(index, question, 8), index);
4283
+ writeAsk(question, searchIndex(index, question, 8), index, config);
3276
4284
  return 0;
3277
4285
  } catch (error) {
3278
4286
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}
@@ -3292,19 +4300,19 @@ Install peers: ${layer1InstallHint()}
3292
4300
  if (kind === "packages") {
3293
4301
  const items2 = index.lookup?.packages ?? [];
3294
4302
  if (wantsTextOutput(flags, config)) writeLines(items2);
3295
- else writeJson({ kind, items: items2 });
4303
+ else writeJson2({ kind, items: items2 });
3296
4304
  return 0;
3297
4305
  }
3298
4306
  if (kind === "intents") {
3299
4307
  const items2 = Object.keys(index.lookup?.intents ?? {});
3300
4308
  if (wantsTextOutput(flags, config)) writeLines(items2);
3301
- else writeJson({ kind, items: items2 });
4309
+ else writeJson2({ kind, items: items2 });
3302
4310
  return 0;
3303
4311
  }
3304
4312
  if (kind === "changes") {
3305
4313
  const items2 = Object.keys(index.lookup?.changes ?? {});
3306
4314
  if (wantsTextOutput(flags, config)) writeLines(items2);
3307
- else writeJson({ kind, items: items2 });
4315
+ else writeJson2({ kind, items: items2 });
3308
4316
  return 0;
3309
4317
  }
3310
4318
  const items = index.knowledge.map((entry) => ({
@@ -3315,7 +4323,7 @@ Install peers: ${layer1InstallHint()}
3315
4323
  if (wantsTextOutput(flags, config)) {
3316
4324
  writeLines(items.map((item) => [item.id, item.path, item.title].join(" ")));
3317
4325
  } else {
3318
- writeJson({ kind, items });
4326
+ writeJson2({ kind, items });
3319
4327
  }
3320
4328
  return 0;
3321
4329
  } catch (error) {