@agentskit/doc-bridge 0.1.0-alpha.1 → 0.1.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/dist/cli/program.js +416 -148
  4. package/dist/cli/program.js.map +1 -1
  5. package/dist/config/index.d.ts +1 -1
  6. package/dist/config/index.js +6 -3
  7. package/dist/config/index.js.map +1 -1
  8. package/dist/{index-CD_zmKXf.d.ts → index-CPUJbTbg.d.ts} +19 -10
  9. package/dist/index.d.ts +25 -6
  10. package/dist/index.js +398 -132
  11. package/dist/index.js.map +1 -1
  12. package/docs/DOGFOOD-ROUND2.md +142 -0
  13. package/docs/DOGFOOD.md +92 -0
  14. package/package.json +32 -14
  15. package/scripts/prepare.mjs +44 -0
  16. package/src/cli/program.ts +947 -0
  17. package/src/config/defaults.ts +63 -0
  18. package/src/config/define-config.ts +6 -0
  19. package/src/config/index.ts +15 -0
  20. package/src/config/load-config.ts +138 -0
  21. package/src/config/schema.ts +294 -0
  22. package/src/federation/llms.ts +154 -0
  23. package/src/gates/run-gates.ts +274 -0
  24. package/src/index-builder/build-handoffs.ts +232 -0
  25. package/src/index-builder/build-index.ts +137 -0
  26. package/src/index-builder/capabilities.ts +37 -0
  27. package/src/index-builder/content-hash.ts +19 -0
  28. package/src/index-builder/human-adapters/core.ts +100 -0
  29. package/src/index-builder/human-adapters/docusaurus.ts +113 -0
  30. package/src/index-builder/human-adapters/fumadocs.ts +70 -0
  31. package/src/index-builder/human-adapters/index.ts +52 -0
  32. package/src/index-builder/human-adapters/plain-markdown.ts +10 -0
  33. package/src/index-builder/llms-txt.ts +19 -0
  34. package/src/index-builder/plugins/human-markdown.ts +7 -0
  35. package/src/index-builder/plugins/pnpm-monorepo.ts +72 -0
  36. package/src/index-builder/scan-corpus.ts +185 -0
  37. package/src/index.ts +120 -0
  38. package/src/intelligence/adapter.ts +90 -0
  39. package/src/intelligence/chat.ts +148 -0
  40. package/src/intelligence/peers.ts +59 -0
  41. package/src/intelligence/rag.ts +98 -0
  42. package/src/lib/glob-expand.ts +33 -0
  43. package/src/lib/markdown.ts +117 -0
  44. package/src/lib/package-manager.ts +104 -0
  45. package/src/lib/paths.ts +6 -0
  46. package/src/lib/walk.ts +45 -0
  47. package/src/mcp/server.ts +276 -0
  48. package/src/memory/ingest.ts +51 -0
  49. package/src/memory/pipeline.ts +119 -0
  50. package/src/query/load-index.ts +25 -0
  51. package/src/query/query.ts +142 -0
  52. package/src/query/search.ts +126 -0
  53. package/src/retriever/doc-bridge-retriever.ts +62 -0
  54. package/src/schemas/agent-handoff.ts +82 -0
  55. package/src/schemas/doc-bridge-index.ts +108 -0
  56. package/src/schemas/json-schemas.ts +157 -0
  57. package/src/schemas/memory-candidate.ts +37 -0
  58. package/src/shims/agentskit-peers.d.ts +80 -0
  59. package/src/validate.ts +67 -0
  60. package/src/version.ts +1 -0
  61. package/tsconfig.json +21 -0
  62. package/tsup.config.ts +15 -0
@@ -0,0 +1,947 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+ import { createInterface } from 'node:readline/promises'
4
+
5
+ import { loadConfig, projectRootFromConfigPath } from '../config/load-config.js'
6
+ import type { DocBridgeConfigV1 } from '../config/schema.js'
7
+ import { buildDocBridgeIndex } from '../index-builder/build-index.js'
8
+ import { discoverPnpmPackages } from '../index-builder/plugins/pnpm-monorepo.js'
9
+ import { scanHumanDocRecords } from '../index-builder/human-adapters/index.js'
10
+ import { retrieveHybridChunks } from '../federation/llms.js'
11
+ import { runGates, type GateId } from '../gates/run-gates.js'
12
+ import { runChatOnce, startInkChat } from '../intelligence/chat.js'
13
+ import { PeerMissingError, layer1InstallHint } from '../intelligence/peers.js'
14
+ import { createDocBridgeRag } from '../intelligence/rag.js'
15
+ import { firstHeading, firstParagraph } from '../lib/markdown.js'
16
+ import { ingestMemoryCandidates } from '../memory/ingest.js'
17
+ import { classifyMemoryCandidates, draftMemoryPromotion } from '../memory/pipeline.js'
18
+ import { startMcpStdioServer } from '../mcp/server.js'
19
+ import { IndexNotFoundError, loadDocBridgeIndex } from '../query/load-index.js'
20
+ import { runQuery, type QueryKind } from '../query/query.js'
21
+ import { searchIndex } from '../query/search.js'
22
+ import type { DocBridgeIndexV1 } from '../schemas/doc-bridge-index.js'
23
+ import { parseAgentHandoff, parseDocBridgeConfig } from '../validate.js'
24
+ import { PACKAGE_VERSION } from '../version.js'
25
+
26
+ type Command =
27
+ | 'help'
28
+ | 'version'
29
+ | 'validate-config'
30
+ | 'validate-handoff'
31
+ | 'init'
32
+ | 'bootstrap'
33
+ | 'memory'
34
+ | 'playbook'
35
+ | 'registry'
36
+ | 'index'
37
+ | 'gate'
38
+ | 'mcp'
39
+ | 'query'
40
+ | 'search'
41
+ | 'retrieve'
42
+ | 'ask'
43
+ | 'chat'
44
+ | 'rag'
45
+ | 'list'
46
+
47
+ const usage = `ak-docs — human↔agent documentation bridge (@agentskit/doc-bridge)
48
+
49
+ Core (no API key):
50
+ ak-docs init [--demo] [--scaffold-workspaces]
51
+ ak-docs index
52
+ ak-docs query <package|ownership|intent|change> <id> [--agent] [--text]
53
+ ak-docs search <term> [--agent] [--text]
54
+ ak-docs list <packages|intents|changes|knowledge> [--text]
55
+ ak-docs ask [question] local consult (no LLM)
56
+ ak-docs gate run [gate-id]
57
+ ak-docs mcp
58
+ ak-docs memory ingest|classify|promote
59
+ ak-docs bootstrap agent-docs
60
+ ak-docs validate-config | validate-handoff <file>
61
+
62
+ Intelligence (optional AgentsKit peers):
63
+ ak-docs rag ingest|search <query>
64
+ ak-docs chat terminal chat (Ink + RAG)
65
+ ak-docs ask <question> --chat one-shot grounded answer
66
+
67
+ Advanced / ecosystem:
68
+ ak-docs retrieve <query>
69
+ ak-docs registry topology
70
+ ak-docs playbook draft
71
+
72
+ Global flags:
73
+ -h, --help --version
74
+ --config <path> (project root = config file directory)
75
+ --agent --json --text --chat --demo
76
+ `
77
+
78
+ const QUERY_KINDS = new Set<QueryKind>(['package', 'ownership', 'intent', 'change', 'search'])
79
+ const LIST_KINDS = new Set(['packages', 'intents', 'changes', 'knowledge'])
80
+ const GATE_IDS = new Set<GateId>(['index-freshness', 'human-guide-links', 'okf-type', 'docs-style'])
81
+
82
+ const parseArgs = (argv: readonly string[]) => {
83
+ const flags = new Set<string>()
84
+ let configPath: string | undefined
85
+ const positional: string[] = []
86
+
87
+ for (let i = 0; i < argv.length; i += 1) {
88
+ const arg = argv[i]
89
+ if (!arg) continue
90
+ if (arg === '--config') {
91
+ configPath = argv[i + 1]
92
+ i += 1
93
+ continue
94
+ }
95
+ if (arg.startsWith('-')) {
96
+ flags.add(arg)
97
+ continue
98
+ }
99
+ positional.push(arg)
100
+ }
101
+
102
+ let command: Command = 'help'
103
+ if (flags.has('--version') || flags.has('-V')) command = 'version'
104
+ else if (flags.has('--help') || flags.has('-h')) command = 'help'
105
+ else if (positional[0] === 'validate-config') command = 'validate-config'
106
+ else if (positional[0] === 'validate-handoff') command = 'validate-handoff'
107
+ else if (positional[0] === 'init') command = 'init'
108
+ else if (positional[0] === 'bootstrap') command = 'bootstrap'
109
+ else if (positional[0] === 'memory') command = 'memory'
110
+ else if (positional[0] === 'playbook') command = 'playbook'
111
+ else if (positional[0] === 'registry') command = 'registry'
112
+ else if (positional[0] === 'index') command = 'index'
113
+ else if (positional[0] === 'gate') command = 'gate'
114
+ else if (positional[0] === 'mcp') command = 'mcp'
115
+ else if (positional[0] === 'query') command = 'query'
116
+ else if (positional[0] === 'search') command = 'search'
117
+ else if (positional[0] === 'retrieve') command = 'retrieve'
118
+ else if (positional[0] === 'ask') command = 'ask'
119
+ else if (positional[0] === 'chat') command = 'chat'
120
+ else if (positional[0] === 'rag') command = 'rag'
121
+ else if (positional[0] === 'list') command = 'list'
122
+
123
+ return { command, flags, configPath, positional }
124
+ }
125
+
126
+ const writeJson = (payload: unknown): void => {
127
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`)
128
+ }
129
+
130
+ const writeLines = (lines: readonly string[]): void => {
131
+ process.stdout.write(lines.length ? `${lines.join('\n')}\n` : '')
132
+ }
133
+
134
+ const textValue = (value: unknown): string => {
135
+ if (value === null || value === undefined) return 'Not found'
136
+ if (typeof value === 'string') return value
137
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value)
138
+ return JSON.stringify(value, null, 2)
139
+ }
140
+
141
+ const writeTextQuery = (payload: unknown): void => {
142
+ if (!payload || typeof payload !== 'object' || !('data' in payload)) {
143
+ writeLines([textValue(payload)])
144
+ return
145
+ }
146
+
147
+ const result = payload as { readonly type?: unknown; readonly data?: unknown }
148
+ if (result.type === 'search') {
149
+ const data = result.data as {
150
+ readonly term?: unknown
151
+ readonly count?: unknown
152
+ readonly matches?: readonly {
153
+ readonly type: string
154
+ readonly id: string
155
+ readonly path: string
156
+ readonly summary?: string
157
+ }[]
158
+ }
159
+ const matches = data.matches ?? []
160
+ writeLines([
161
+ `Search: ${String(data.term ?? '')}`,
162
+ `Matches: ${String(data.count ?? matches.length)}`,
163
+ ...(matches.length
164
+ ? matches.map((match) =>
165
+ formatSearchMatch(match as { type: string; id: string; path: string; summary?: string }),
166
+ )
167
+ : [' (none)']),
168
+ ])
169
+ return
170
+ }
171
+
172
+ writeLines([textValue(result.data)])
173
+ }
174
+
175
+ const formatSearchMatch = (match: {
176
+ readonly type: string
177
+ readonly id: string
178
+ readonly path: string
179
+ readonly summary?: string
180
+ readonly score?: number
181
+ }): string => {
182
+ const summary = match.summary ? match.summary.replace(/\s+/g, ' ').slice(0, 100) : ''
183
+ const score = typeof match.score === 'number' ? ` score=${match.score}` : ''
184
+ return ` [${match.type}] ${match.id}${score}\n ${match.path}${summary ? `\n ${summary}` : ''}`
185
+ }
186
+
187
+ const writeTextSearch = (
188
+ term: string,
189
+ matches: readonly {
190
+ readonly type: string
191
+ readonly id: string
192
+ readonly path: string
193
+ readonly summary?: string
194
+ readonly score?: number
195
+ }[],
196
+ ): void => {
197
+ writeLines([
198
+ `Search: ${term}`,
199
+ `Matches: ${matches.length}`,
200
+ ...(matches.length ? matches.map(formatSearchMatch) : [' (none)']),
201
+ ])
202
+ }
203
+
204
+ const writeAsk = (
205
+ question: string,
206
+ matches: ReturnType<typeof searchIndex>,
207
+ index: DocBridgeIndexV1,
208
+ ): void => {
209
+ // Prefer ownership match for routing questions
210
+ const owner =
211
+ matches.find((match) => match.type === 'ownership') ??
212
+ matches.find((match) => Boolean(index.lookup?.ownership?.[match.id]))
213
+ const best = owner ?? matches[0]
214
+ const bestQuery =
215
+ best && (best.type === 'ownership' || index.lookup?.ownership?.[best.id])
216
+ ? `ak-docs query ownership ${best.id} --agent`
217
+ : 'ak-docs list knowledge --text'
218
+ writeLines([
219
+ `Question: ${question}`,
220
+ best ? `Best match: ${best.type} ${best.id} (${best.path})` : 'Best match: none',
221
+ '',
222
+ 'Matches:',
223
+ ...(
224
+ matches.length
225
+ ? matches.slice(0, 5).map(formatSearchMatch)
226
+ : [' No local matches. Try: ak-docs search <term>']
227
+ ),
228
+ '',
229
+ 'Next commands:',
230
+ ...(best
231
+ ? [
232
+ `ak-docs search "${question}" --agent`,
233
+ bestQuery,
234
+ ]
235
+ : ['ak-docs list knowledge --text']),
236
+ ])
237
+ }
238
+
239
+ const readIndexedDoc = (root: string, config: DocBridgeConfigV1, idOrPath: string): string => {
240
+ const index = loadDocBridgeIndex(root, config)
241
+ const entry = index.knowledge.find((doc) => doc.id === idOrPath || doc.path === idOrPath)
242
+ if (!entry) throw new Error(`Unknown indexed doc "${idOrPath}". Try: search ${idOrPath}`)
243
+
244
+ const abs = resolve(root, entry.path)
245
+ const rootAbs = resolve(root)
246
+ if (abs !== rootAbs && !abs.startsWith(`${rootAbs}/`)) {
247
+ throw new Error(`Indexed doc escapes project root: ${entry.path}`)
248
+ }
249
+ return readFileSync(abs, 'utf8')
250
+ }
251
+
252
+ const runAskRepl = async (root: string, config: DocBridgeConfigV1): Promise<number> => {
253
+ const index = loadDocBridgeIndex(root, config)
254
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true })
255
+ try {
256
+ for (;;) {
257
+ const line = (await rl.question('ak-docs> ')).trim()
258
+ if (!line) continue
259
+ if (line === 'exit' || line === 'quit') return 0
260
+
261
+ const [command, ...rest] = line.split(/\s+/)
262
+ const value = rest.join(' ').trim()
263
+ try {
264
+ if (command === 'search') {
265
+ writeTextSearch(value, searchIndex(index, value))
266
+ } else if (command === 'read' || command === 'open') {
267
+ process.stdout.write(`${readIndexedDoc(root, config, value).slice(0, 8000)}\n`)
268
+ } else if (command === 'resolve') {
269
+ writeJson(runQuery(index, config, { kind: 'ownership', id: value, agent: true }))
270
+ } else if (command === 'gate') {
271
+ const gateId = value || undefined
272
+ writeJson(runGates(root, config, gateId ? [gateId as GateId] : undefined))
273
+ } else {
274
+ writeAsk(line, searchIndex(index, line, 8), index)
275
+ }
276
+ } catch (error) {
277
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
278
+ }
279
+ }
280
+ } finally {
281
+ rl.close()
282
+ }
283
+ }
284
+
285
+ const wantsTextOutput = (flags: ReadonlySet<string>, config: DocBridgeConfigV1): boolean =>
286
+ !flags.has('--agent') &&
287
+ (flags.has('--text') || (!flags.has('--json') && config.surfaces?.cli?.defaultFormat === 'text'))
288
+
289
+ const loadProject = (configPath?: string) => {
290
+ const loadOpts = configPath ? { explicitPath: configPath } : {}
291
+ const { config, path } = loadConfig(loadOpts)
292
+ parseDocBridgeConfig(config)
293
+ const root = projectRootFromConfigPath(path, config.project?.root)
294
+ return { config, configPath: path, root }
295
+ }
296
+
297
+ const indexDiagnostics = (config: DocBridgeConfigV1, result: ReturnType<typeof buildDocBridgeIndex>): string[] => {
298
+ const diagnostics: string[] = []
299
+ const onlyDoc = result.index.knowledge.length === 1 ? result.index.knowledge[0] : undefined
300
+ if (onlyDoc?.path === config.corpus.agent.index) {
301
+ diagnostics.push(
302
+ `Only the starter ${config.corpus.agent.index} was indexed.`,
303
+ `Add agent docs under ${config.corpus.agent.root}/, then run ak-docs index again.`,
304
+ )
305
+ }
306
+ const handoffCount = Object.keys(result.index.handoffs ?? {}).length
307
+ if (handoffCount === 0) {
308
+ diagnostics.push(
309
+ 'No ownership handoffs yet. Add routing.options.ownership, package frontmatter (package + editRoot), or a monorepo plugin.',
310
+ )
311
+ }
312
+ return diagnostics
313
+ }
314
+
315
+ const diagnosticNextCommands = (
316
+ config: DocBridgeConfigV1,
317
+ result: ReturnType<typeof buildDocBridgeIndex>,
318
+ ): string[] => {
319
+ const handoffIds = Object.keys(result.index.handoffs ?? {})
320
+ if (handoffIds[0]) {
321
+ return [
322
+ `ak-docs query package ${handoffIds[0]} --agent`,
323
+ `ak-docs list packages --text`,
324
+ 'ak-docs mcp',
325
+ ]
326
+ }
327
+ return [
328
+ `mkdir -p ${config.corpus.agent.root}/packages`,
329
+ `edit ${config.corpus.agent.root}/packages/<module>.md # frontmatter: package + editRoot`,
330
+ 'ak-docs index',
331
+ ]
332
+ }
333
+
334
+ const writeIfMissing = (path: string, contents: string): boolean => {
335
+ if (existsSync(path)) return false
336
+ mkdirSync(dirname(path), { recursive: true })
337
+ writeFileSync(path, contents, 'utf8')
338
+ return true
339
+ }
340
+
341
+ const demoOwnership = {
342
+ example: {
343
+ path: 'src',
344
+ purpose: 'Starter ownership target — replace with your real modules',
345
+ checks: ['npm test'],
346
+ agentDoc: 'docs/for-agents/packages/example.md',
347
+ },
348
+ }
349
+
350
+ const initConfigObject = (withDemo: boolean) => ({
351
+ schemaVersion: 1 as const,
352
+ corpus: { agent: { root: 'docs/for-agents' } },
353
+ ...(withDemo
354
+ ? {
355
+ routing: {
356
+ options: {
357
+ ownership: demoOwnership,
358
+ },
359
+ },
360
+ }
361
+ : {}),
362
+ gates: { preset: 'minimal' as const },
363
+ })
364
+
365
+ const initConfigContents = (path: string, withDemo: boolean): string => {
366
+ if (path.endsWith('.ts') || path.endsWith('.mts')) {
367
+ return [
368
+ "import { defineConfig } from '@agentskit/doc-bridge/config'",
369
+ '',
370
+ 'export default defineConfig({',
371
+ ' schemaVersion: 1,',
372
+ " corpus: { agent: { root: 'docs/for-agents' } },",
373
+ ...(withDemo
374
+ ? [
375
+ ' routing: {',
376
+ ' options: {',
377
+ ' ownership: {',
378
+ " example: { path: 'src', purpose: 'Starter ownership target', checks: ['npm test'], agentDoc: 'docs/for-agents/packages/example.md' },",
379
+ ' },',
380
+ ' },',
381
+ ' },',
382
+ ]
383
+ : []),
384
+ " gates: { preset: 'minimal' },",
385
+ '})',
386
+ '',
387
+ ].join('\n')
388
+ }
389
+
390
+ return `${JSON.stringify(initConfigObject(withDemo), null, 2)}\n`
391
+ }
392
+
393
+ const exampleAgentDoc = `---
394
+ type: package
395
+ package: example
396
+ editRoot: src
397
+ checks: [npm test]
398
+ ---
399
+
400
+ # example
401
+
402
+ Starter agent doc generated by \`ak-docs init --demo\`.
403
+
404
+ Describe ownership, boundaries, and how agents should change this module.
405
+
406
+ ## Checks
407
+
408
+ - \`npm test\`
409
+ `
410
+
411
+ const agentsMdSnippet = `# AGENTS.md
412
+
413
+ ## Documentation routing (doc-bridge)
414
+
415
+ Before editing a package or module:
416
+
417
+ 1. Run \`ak-docs query ownership <id> --agent\` (or MCP tool \`handoff.resolve\`)
418
+ 2. Read \`startHere\` and respect \`editRoots\` + \`checks\`
419
+ 3. Prefer agent docs under \`docs/for-agents/\`; human site links appear as \`humanDoc\`
420
+
421
+ Local consult without LLM: \`ak-docs ask "<question>"\`
422
+ Optional grounded chat (AgentsKit peers): \`ak-docs chat\`
423
+ `
424
+
425
+ const workspaceDocDraft = (id: string, path: string): string => [
426
+ '---',
427
+ 'type: package',
428
+ 'draft: true',
429
+ `package: ${id}`,
430
+ `editRoot: ${path}`,
431
+ '---',
432
+ '',
433
+ `# ${id}`,
434
+ '',
435
+ 'Draft generated by `ak-docs init --scaffold-workspaces`.',
436
+ '',
437
+ '## Ownership',
438
+ '',
439
+ `- Package: \`${path}\``,
440
+ '',
441
+ '## Notes',
442
+ '',
443
+ '- TODO: describe responsibility, boundaries, and checks.',
444
+ '',
445
+ ].join('\n')
446
+
447
+ const scaffoldWorkspaceDocs = (
448
+ root: string,
449
+ config: DocBridgeConfigV1,
450
+ ): { created: string[]; skipped: string[] } => {
451
+ const created: string[] = []
452
+ const skipped: string[] = []
453
+ for (const pkg of discoverPnpmPackages(root, config)) {
454
+ const path = resolve(root, config.corpus.agent.root, 'packages', `${pkg.id}.md`)
455
+ if (writeIfMissing(path, workspaceDocDraft(pkg.id, pkg.path))) created.push(path)
456
+ else skipped.push(path)
457
+ }
458
+ return { created, skipped }
459
+ }
460
+
461
+ const bootstrapAgentDocs = (
462
+ root: string,
463
+ config: DocBridgeConfigV1,
464
+ ): { created: string[]; skipped: string[] } => {
465
+ const created: string[] = []
466
+ const skipped: string[] = []
467
+ for (const doc of scanHumanDocRecords(root, config)) {
468
+ const raw = readFileSync(doc.path, 'utf8')
469
+ const body = raw.replace(/^---\n[\s\S]*?\n---\n?/, '')
470
+ const title = firstHeading(body) ?? doc.id
471
+ const description = firstParagraph(body)
472
+ const draftPath = resolve(root, config.corpus.agent.root, 'human', `${doc.id}.md`)
473
+ const draft = [
474
+ '---',
475
+ 'type: knowledge',
476
+ 'draft: true',
477
+ `id: ${doc.id}`,
478
+ `humanDoc: ${doc.url}`,
479
+ '---',
480
+ '',
481
+ `# ${title}`,
482
+ '',
483
+ 'Draft generated by `ak-docs bootstrap agent-docs` from existing human docs.',
484
+ '',
485
+ ...(description ? ['## Source summary', '', description, ''] : []),
486
+ '## Review checklist',
487
+ '',
488
+ '- TODO: confirm ownership, edit roots, and checks.',
489
+ '- TODO: move this draft to the right agent-doc location if needed.',
490
+ '',
491
+ ].join('\n')
492
+ if (writeIfMissing(draftPath, draft)) created.push(draftPath)
493
+ else skipped.push(draftPath)
494
+ }
495
+ return { created, skipped }
496
+ }
497
+
498
+ const registryTopology = () => ({
499
+ id: 'doc-curator',
500
+ delegates: ['docs-chat', 'knowledge-promoter', 'code-review'],
501
+ tools: ['handoff.resolve', 'doc.search', 'doc.get', 'gate.status', 'retriever.query'],
502
+ steps: ['classify', 'draft', 'verify', 'review'],
503
+ mergePolicy: { autoMerge: false, requiresHuman: true },
504
+ })
505
+
506
+ export const runCli = (argv: readonly string[]): number | undefined | Promise<number> => {
507
+ const { command, flags, configPath, positional } = parseArgs(argv)
508
+
509
+ if (command === 'help') {
510
+ process.stdout.write(usage)
511
+ return 0
512
+ }
513
+
514
+ if (command === 'version') {
515
+ process.stdout.write(`ak-docs ${PACKAGE_VERSION} (@agentskit/doc-bridge)\n`)
516
+ return 0
517
+ }
518
+
519
+ if (command === 'validate-config') {
520
+ try {
521
+ const { config, path } = loadConfig(
522
+ configPath ? { explicitPath: configPath } : {},
523
+ )
524
+ parseDocBridgeConfig(config)
525
+ writeJson({ ok: true, path, schemaVersion: config.schemaVersion })
526
+ return 0
527
+ } catch (error) {
528
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
529
+ return 1
530
+ }
531
+ }
532
+
533
+ if (command === 'validate-handoff') {
534
+ const file = positional[1]
535
+ if (!file) {
536
+ process.stderr.write('Missing handoff JSON file path.\n')
537
+ return 1
538
+ }
539
+ try {
540
+ const abs = resolve(file)
541
+ const raw = readFileSync(abs, 'utf8')
542
+ const handoff = parseAgentHandoff(JSON.parse(raw) as unknown)
543
+ writeJson({ ok: true, handoff })
544
+ return 0
545
+ } catch (error) {
546
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
547
+ return 1
548
+ }
549
+ }
550
+
551
+ if (command === 'init') {
552
+ const root = process.cwd()
553
+ const withDemo = flags.has('--demo') || !flags.has('--no-demo')
554
+ const configFile = resolve(root, configPath ?? 'doc-bridge.config.json')
555
+ const docsIndex = resolve(root, 'docs/for-agents/INDEX.md')
556
+ const exampleDoc = resolve(root, 'docs/for-agents/packages/example.md')
557
+ const agentsMd = resolve(root, 'AGENTS.md')
558
+ const configWritten = writeIfMissing(configFile, initConfigContents(configFile, withDemo))
559
+ const indexWritten = writeIfMissing(
560
+ docsIndex,
561
+ withDemo
562
+ ? '# Agent docs index\n\n- [example](./packages/example.md) — starter ownership target\n'
563
+ : '# Agent docs index\n\nStart here for ownership, architecture, and task handoffs.\n',
564
+ )
565
+ const exampleWritten = withDemo ? writeIfMissing(exampleDoc, exampleAgentDoc) : false
566
+ const agentsWritten = writeIfMissing(agentsMd, agentsMdSnippet)
567
+ if (withDemo) writeIfMissing(resolve(root, 'src/.gitkeep'), '')
568
+ const scaffold = flags.has('--scaffold-workspaces')
569
+ ? scaffoldWorkspaceDocs(root, loadProject(configFile).config)
570
+ : undefined
571
+ writeJson({
572
+ ok: true,
573
+ configPath: configFile,
574
+ demo: withDemo,
575
+ created: {
576
+ config: configWritten,
577
+ index: indexWritten,
578
+ ...(withDemo ? { exampleDoc: exampleWritten, srcStub: true } : {}),
579
+ agentsMd: agentsWritten,
580
+ ...(scaffold ? { workspaceDocs: scaffold.created } : {}),
581
+ },
582
+ ...(scaffold ? { skipped: { workspaceDocs: scaffold.skipped } } : {}),
583
+ nextCommands: withDemo
584
+ ? ['ak-docs index', 'ak-docs query package example --agent', 'ak-docs list packages --text']
585
+ : ['ak-docs index', 'ak-docs list knowledge --text'],
586
+ })
587
+ return 0
588
+ }
589
+
590
+ if (command === 'bootstrap') {
591
+ if (positional[1] !== 'agent-docs') {
592
+ process.stderr.write('Usage: ak-docs bootstrap agent-docs [--config <path>]\n')
593
+ return 1
594
+ }
595
+ try {
596
+ const { config, root } = loadProject(configPath)
597
+ const result = bootstrapAgentDocs(root, config)
598
+ writeJson({ ok: true, ...result })
599
+ return 0
600
+ } catch (error) {
601
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
602
+ return 1
603
+ }
604
+ }
605
+
606
+ if (command === 'memory') {
607
+ if (!['ingest', 'classify', 'promote'].includes(positional[1] ?? '')) {
608
+ process.stderr.write('Usage: ak-docs memory <ingest|classify|promote> [--config <path>]\n')
609
+ return 1
610
+ }
611
+ try {
612
+ const { config, root } = loadProject(configPath)
613
+ const candidates = ingestMemoryCandidates(root)
614
+ if (positional[1] === 'ingest') {
615
+ writeJson({ ok: true, count: candidates.length, candidates })
616
+ return 0
617
+ }
618
+ const index = loadDocBridgeIndex(root, config)
619
+ const classifications = classifyMemoryCandidates(candidates, index)
620
+ if (positional[1] === 'classify') {
621
+ writeJson({ ok: true, count: classifications.length, classifications })
622
+ return 0
623
+ }
624
+ const draft = draftMemoryPromotion(classifications)
625
+ writeJson(draft)
626
+ return draft.ok ? 0 : 1
627
+ } catch (error) {
628
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
629
+ return 1
630
+ }
631
+ }
632
+
633
+ if (command === 'registry') {
634
+ if (positional[1] !== 'topology') {
635
+ process.stderr.write('Usage: ak-docs registry topology\n')
636
+ return 1
637
+ }
638
+ writeJson(registryTopology())
639
+ return 0
640
+ }
641
+
642
+ if (command === 'playbook') {
643
+ if (positional[1] !== 'draft') {
644
+ process.stderr.write('Usage: ak-docs playbook draft [--config <path>]\n')
645
+ return 1
646
+ }
647
+ try {
648
+ const { config, root } = loadProject(configPath)
649
+ const index = loadDocBridgeIndex(root, config)
650
+ const draft = draftMemoryPromotion(classifyMemoryCandidates(ingestMemoryCandidates(root), index))
651
+ writeJson({
652
+ ...draft,
653
+ title: 'Draft Playbook feedback promotion',
654
+ pattern: 'Doc Bridge Pattern',
655
+ })
656
+ return 0
657
+ } catch (error) {
658
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
659
+ return 1
660
+ }
661
+ }
662
+
663
+ if (command === 'index') {
664
+ try {
665
+ const { config, root } = loadProject(configPath)
666
+ const result = buildDocBridgeIndex({ root, config })
667
+ const diagnostics = indexDiagnostics(config, result)
668
+ const handoffCount = Object.keys(result.index.handoffs ?? {}).length
669
+ writeJson({
670
+ ok: true,
671
+ indexPath: result.indexPath,
672
+ ...(result.llmsTxtPath ? { llmsTxtPath: result.llmsTxtPath } : {}),
673
+ ...(result.capabilitiesPath ? { capabilitiesPath: result.capabilitiesPath } : {}),
674
+ contentHash: result.index.contentHash,
675
+ knowledgeCount: result.index.knowledge.length,
676
+ packageCount: result.index.lookup?.packages.length ?? 0,
677
+ handoffCount,
678
+ ...(diagnostics.length ? { diagnostics } : {}),
679
+ nextCommands: diagnosticNextCommands(config, result),
680
+ })
681
+ return 0
682
+ } catch (error) {
683
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
684
+ return 1
685
+ }
686
+ }
687
+
688
+ if (command === 'gate') {
689
+ const action = positional[1]
690
+ const gateId = positional[2] as GateId | undefined
691
+ if (action !== 'run') {
692
+ process.stderr.write('Usage: ak-docs gate run [index-freshness]\n')
693
+ return 1
694
+ }
695
+ if (gateId && !GATE_IDS.has(gateId)) {
696
+ process.stderr.write(
697
+ `Unsupported gate "${gateId}". Supported gates: index-freshness, human-guide-links, okf-type, docs-style\n`,
698
+ )
699
+ return 1
700
+ }
701
+ try {
702
+ const { config, root } = loadProject(configPath)
703
+ const result = runGates(root, config, gateId ? [gateId] : undefined)
704
+ writeJson(result)
705
+ return result.ok ? 0 : 1
706
+ } catch (error) {
707
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
708
+ return 1
709
+ }
710
+ }
711
+
712
+ if (command === 'mcp') {
713
+ try {
714
+ const { config, root } = loadProject(configPath)
715
+ startMcpStdioServer({ root, config })
716
+ return undefined
717
+ } catch (error) {
718
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
719
+ return 1
720
+ }
721
+ }
722
+
723
+ if (command === 'query') {
724
+ const kind = positional[1] as QueryKind | undefined
725
+ const id = positional[2]
726
+ if (!kind || !QUERY_KINDS.has(kind) || kind === 'search') {
727
+ process.stderr.write('Usage: ak-docs query <package|ownership|intent|change> <id> [--agent]\n')
728
+ return 1
729
+ }
730
+ if (!id) {
731
+ process.stderr.write(`Missing id for query kind "${kind}".\n`)
732
+ return 1
733
+ }
734
+ try {
735
+ const { config, root } = loadProject(configPath)
736
+ const index = loadDocBridgeIndex(root, config)
737
+ const result = runQuery(index, config, { kind, id, agent: flags.has('--agent') })
738
+ if (wantsTextOutput(flags, config)) writeTextQuery(result)
739
+ else writeJson(result)
740
+ return 0
741
+ } catch (error) {
742
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
743
+ return 1
744
+ }
745
+ }
746
+
747
+ if (command === 'search') {
748
+ const term = positional.slice(1).join(' ').trim()
749
+ if (!term) {
750
+ process.stderr.write('Usage: ak-docs search <term> [--agent]\n')
751
+ return 1
752
+ }
753
+ try {
754
+ const { config, root } = loadProject(configPath)
755
+ const index = loadDocBridgeIndex(root, config)
756
+ if (flags.has('--agent')) {
757
+ const result = runQuery(index, config, { kind: 'search', term, agent: true })
758
+ writeJson(result)
759
+ } else {
760
+ const matches = searchIndex(index, term)
761
+ if (wantsTextOutput(flags, config)) writeTextSearch(term, matches)
762
+ else writeJson({ term, count: matches.length, matches })
763
+ }
764
+ return 0
765
+ } catch (error) {
766
+ if (error instanceof IndexNotFoundError) {
767
+ process.stderr.write(`${error.message}\n`)
768
+ return 1
769
+ }
770
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
771
+ return 1
772
+ }
773
+ }
774
+
775
+ if (command === 'retrieve') {
776
+ const query = positional.slice(1).join(' ').trim()
777
+ if (!query) {
778
+ process.stderr.write('Usage: ak-docs retrieve <query> [--config <path>]\n')
779
+ return 1
780
+ }
781
+ return (async () => {
782
+ try {
783
+ const { config, root } = loadProject(configPath)
784
+ const index = loadDocBridgeIndex(root, config)
785
+ writeJson({ query, chunks: await retrieveHybridChunks(root, config, index, query) })
786
+ return 0
787
+ } catch (error) {
788
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
789
+ return 1
790
+ }
791
+ })()
792
+ }
793
+
794
+ if (command === 'rag') {
795
+ const action = positional[1]
796
+ if (action !== 'ingest' && action !== 'search') {
797
+ process.stderr.write('Usage: ak-docs rag ingest | ak-docs rag search <query>\n')
798
+ return 1
799
+ }
800
+ return (async () => {
801
+ try {
802
+ const { config, root } = loadProject(configPath)
803
+ const index = loadDocBridgeIndex(root, config)
804
+ const rag = await createDocBridgeRag(root, config, index)
805
+ if (action === 'ingest') {
806
+ const result = await rag.ingest()
807
+ writeJson({ ok: true, ...result })
808
+ return 0
809
+ }
810
+ const query = positional.slice(2).join(' ').trim()
811
+ if (!query) {
812
+ process.stderr.write('Usage: ak-docs rag search <query>\n')
813
+ return 1
814
+ }
815
+ const hits = await rag.search(query)
816
+ writeJson({ query, count: hits.length, hits })
817
+ return 0
818
+ } catch (error) {
819
+ if (error instanceof PeerMissingError) {
820
+ process.stderr.write(`${error.message}\n`)
821
+ return 1
822
+ }
823
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
824
+ return 1
825
+ }
826
+ })()
827
+ }
828
+
829
+ if (command === 'chat') {
830
+ return (async () => {
831
+ try {
832
+ const { config, root } = loadProject(configPath)
833
+ if (!config.intelligence?.enabled || !config.intelligence.adapter) {
834
+ process.stderr.write(
835
+ 'Chat requires intelligence.enabled and intelligence.adapter in doc-bridge config.\n' +
836
+ `Layer 1 peers: ${layer1InstallHint()}\n`,
837
+ )
838
+ return 1
839
+ }
840
+ const index = loadDocBridgeIndex(root, config)
841
+ await startInkChat(root, config, index)
842
+ return 0
843
+ } catch (error) {
844
+ if (error instanceof PeerMissingError) {
845
+ process.stderr.write(`${error.message}\n`)
846
+ return 1
847
+ }
848
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
849
+ return 1
850
+ }
851
+ })()
852
+ }
853
+
854
+ if (command === 'ask') {
855
+ const question = positional.slice(1).join(' ').trim()
856
+ try {
857
+ const { config, root } = loadProject(configPath)
858
+ if (flags.has('--chat')) {
859
+ if (!config.intelligence?.enabled || !config.intelligence.adapter) {
860
+ process.stderr.write(
861
+ 'Chat mode requires intelligence.enabled and intelligence.adapter in doc-bridge config.\n' +
862
+ `Install peers: ${layer1InstallHint()}\n`,
863
+ )
864
+ return 1
865
+ }
866
+ if (!question) {
867
+ process.stderr.write('Usage: ak-docs ask <question> --chat\n')
868
+ return 1
869
+ }
870
+ return (async () => {
871
+ try {
872
+ const index = loadDocBridgeIndex(root, config)
873
+ const result = await runChatOnce(root, config, index, question)
874
+ writeLines([result.content])
875
+ return 0
876
+ } catch (error) {
877
+ if (error instanceof PeerMissingError) {
878
+ process.stderr.write(`${error.message}\n`)
879
+ return 1
880
+ }
881
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
882
+ return 1
883
+ }
884
+ })()
885
+ }
886
+ if (!question) {
887
+ if (process.stdin.isTTY) return runAskRepl(root, config)
888
+ process.stderr.write('Usage: ak-docs ask <question>, or run ak-docs ask in an interactive terminal.\n')
889
+ return 1
890
+ }
891
+ const index = loadDocBridgeIndex(root, config)
892
+ writeAsk(question, searchIndex(index, question, 8), index)
893
+ return 0
894
+ } catch (error) {
895
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
896
+ return 1
897
+ }
898
+ }
899
+
900
+ if (command === 'list') {
901
+ const kind = positional[1]
902
+ if (!kind || !LIST_KINDS.has(kind)) {
903
+ process.stderr.write('Usage: ak-docs list <packages|intents|changes|knowledge>\n')
904
+ return 1
905
+ }
906
+ try {
907
+ const { config, root } = loadProject(configPath)
908
+ const index = loadDocBridgeIndex(root, config)
909
+
910
+ if (kind === 'packages') {
911
+ const items = index.lookup?.packages ?? []
912
+ if (wantsTextOutput(flags, config)) writeLines(items)
913
+ else writeJson({ kind, items })
914
+ return 0
915
+ }
916
+ if (kind === 'intents') {
917
+ const items = Object.keys(index.lookup?.intents ?? {})
918
+ if (wantsTextOutput(flags, config)) writeLines(items)
919
+ else writeJson({ kind, items })
920
+ return 0
921
+ }
922
+ if (kind === 'changes') {
923
+ const items = Object.keys(index.lookup?.changes ?? {})
924
+ if (wantsTextOutput(flags, config)) writeLines(items)
925
+ else writeJson({ kind, items })
926
+ return 0
927
+ }
928
+ const items = index.knowledge.map((entry) => ({
929
+ id: entry.id,
930
+ title: entry.title,
931
+ path: entry.path,
932
+ }))
933
+ if (wantsTextOutput(flags, config)) {
934
+ writeLines(items.map((item) => [item.id, item.path, item.title].join('\t')))
935
+ } else {
936
+ writeJson({ kind, items })
937
+ }
938
+ return 0
939
+ } catch (error) {
940
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
941
+ return 1
942
+ }
943
+ }
944
+
945
+ process.stdout.write(usage)
946
+ return 1
947
+ }