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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/dist/cli/program.js +283 -105
  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 +5 -4
  10. package/dist/index.js +278 -100
  11. package/dist/index.js.map +1 -1
  12. package/docs/DOGFOOD.md +92 -0
  13. package/package.json +27 -9
  14. package/scripts/prepare.mjs +44 -0
  15. package/src/cli/program.ts +933 -0
  16. package/src/config/defaults.ts +63 -0
  17. package/src/config/define-config.ts +6 -0
  18. package/src/config/index.ts +15 -0
  19. package/src/config/load-config.ts +138 -0
  20. package/src/config/schema.ts +294 -0
  21. package/src/federation/llms.ts +145 -0
  22. package/src/gates/run-gates.ts +274 -0
  23. package/src/index-builder/build-handoffs.ts +217 -0
  24. package/src/index-builder/build-index.ts +137 -0
  25. package/src/index-builder/capabilities.ts +37 -0
  26. package/src/index-builder/content-hash.ts +19 -0
  27. package/src/index-builder/human-adapters/core.ts +100 -0
  28. package/src/index-builder/human-adapters/docusaurus.ts +113 -0
  29. package/src/index-builder/human-adapters/fumadocs.ts +70 -0
  30. package/src/index-builder/human-adapters/index.ts +52 -0
  31. package/src/index-builder/human-adapters/plain-markdown.ts +10 -0
  32. package/src/index-builder/llms-txt.ts +19 -0
  33. package/src/index-builder/plugins/human-markdown.ts +7 -0
  34. package/src/index-builder/plugins/pnpm-monorepo.ts +72 -0
  35. package/src/index-builder/scan-corpus.ts +181 -0
  36. package/src/index.ts +120 -0
  37. package/src/intelligence/adapter.ts +90 -0
  38. package/src/intelligence/chat.ts +148 -0
  39. package/src/intelligence/peers.ts +59 -0
  40. package/src/intelligence/rag.ts +98 -0
  41. package/src/lib/glob-expand.ts +33 -0
  42. package/src/lib/markdown.ts +90 -0
  43. package/src/lib/package-manager.ts +104 -0
  44. package/src/lib/paths.ts +6 -0
  45. package/src/lib/walk.ts +45 -0
  46. package/src/mcp/server.ts +276 -0
  47. package/src/memory/ingest.ts +51 -0
  48. package/src/memory/pipeline.ts +119 -0
  49. package/src/query/load-index.ts +25 -0
  50. package/src/query/query.ts +142 -0
  51. package/src/query/search.ts +58 -0
  52. package/src/retriever/doc-bridge-retriever.ts +62 -0
  53. package/src/schemas/agent-handoff.ts +82 -0
  54. package/src/schemas/doc-bridge-index.ts +106 -0
  55. package/src/schemas/json-schemas.ts +156 -0
  56. package/src/schemas/memory-candidate.ts +37 -0
  57. package/src/shims/agentskit-peers.d.ts +80 -0
  58. package/src/validate.ts +67 -0
  59. package/src/version.ts +1 -0
  60. package/tsconfig.json +21 -0
  61. package/tsup.config.ts +15 -0
@@ -0,0 +1,100 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { HumanCorpusConfig } from '../../config/schema.js'
5
+ import { slugFromPath } from '../../lib/markdown.js'
6
+ import { toPosix } from '../../lib/paths.js'
7
+ import { walkFiles } from '../../lib/walk.js'
8
+
9
+ export type HumanDocRecord = {
10
+ readonly id: string
11
+ readonly url: string
12
+ readonly path: string
13
+ }
14
+
15
+ export type HumanDocMap = Record<string, string>
16
+
17
+ export type HumanAdapterContext = {
18
+ readonly root: string
19
+ readonly config: HumanCorpusConfig
20
+ }
21
+
22
+ export type HumanAdapter = {
23
+ readonly plugin: HumanCorpusConfig['plugin']
24
+ readonly scan: (ctx: HumanAdapterContext) => HumanDocRecord[]
25
+ }
26
+
27
+ export const optionString = (
28
+ options: Record<string, unknown> | undefined,
29
+ keys: readonly string[],
30
+ ): string | undefined => {
31
+ for (const key of keys) {
32
+ const value = options?.[key]
33
+ if (typeof value === 'string' && value) return value
34
+ }
35
+ return undefined
36
+ }
37
+
38
+ export const parseFrontmatter = (raw: string): Record<string, string> => {
39
+ if (!raw.startsWith('---\n')) return {}
40
+ const end = raw.indexOf('\n---', 4)
41
+ if (end === -1) return {}
42
+
43
+ const out: Record<string, string> = {}
44
+ for (const line of raw.slice(4, end).split('\n')) {
45
+ const match = /^([A-Za-z0-9_-]+):\s*(.+?)\s*$/.exec(line)
46
+ if (match?.[1] && match[2]) out[match[1]] = match[2].replace(/^['"]|['"]$/g, '')
47
+ }
48
+ return out
49
+ }
50
+
51
+ export const docId = (relToHumanRoot: string, raw: string): string => {
52
+ const meta = parseFrontmatter(raw)
53
+ return meta.package ?? meta.module ?? meta.id ?? slugFromPath(relToHumanRoot)
54
+ }
55
+
56
+ export const routeSlug = (relToHumanRoot: string, opts?: { stripGroups?: boolean }): string =>
57
+ relToHumanRoot
58
+ .split('/')
59
+ .filter((part) => !opts?.stripGroups || !/^\(.+\)$/.test(part))
60
+ .join('/')
61
+ .replace(/(?:^|\/)index\.mdx?$/, '')
62
+ .replace(/\.mdx?$/, '')
63
+
64
+ export const humanUrl = (slug: string, urlPrefix: unknown): string => {
65
+ if (typeof urlPrefix !== 'string' || !urlPrefix) return slug
66
+ const prefix = urlPrefix.replace(/\/$/, '')
67
+ return slug ? `${prefix}/${slug.replace(/^\//, '')}` : prefix
68
+ }
69
+
70
+ export const scanMarkdownDocs = (
71
+ root: string,
72
+ humanRoot: string,
73
+ options?: {
74
+ readonly includeRelPath?: (relToHumanRoot: string, raw: string) => boolean
75
+ readonly idForDoc?: (relToHumanRoot: string, raw: string) => string
76
+ readonly slugForDoc?: (relToHumanRoot: string, raw: string) => string
77
+ readonly urlPrefix?: unknown
78
+ readonly stripGroups?: boolean
79
+ },
80
+ ): HumanDocRecord[] => {
81
+ const out: HumanDocRecord[] = []
82
+ const absRoot = join(root, humanRoot)
83
+
84
+ for (const abs of walkFiles(absRoot, { extensions: ['.md', '.mdx'] })) {
85
+ const relToHumanRoot = toPosix(abs.replace(`${toPosix(absRoot)}/`, ''))
86
+ const raw = readFileSync(abs, 'utf8')
87
+ if (options?.includeRelPath && !options.includeRelPath(relToHumanRoot, raw)) continue
88
+ out.push({
89
+ id: options?.idForDoc?.(relToHumanRoot, raw) ?? docId(relToHumanRoot, raw),
90
+ url: humanUrl(
91
+ options?.slugForDoc?.(relToHumanRoot, raw) ??
92
+ routeSlug(relToHumanRoot, options?.stripGroups ? { stripGroups: true } : undefined),
93
+ options?.urlPrefix,
94
+ ),
95
+ path: abs,
96
+ })
97
+ }
98
+
99
+ return out
100
+ }
@@ -0,0 +1,113 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import vm from 'node:vm'
4
+
5
+ import {
6
+ optionString,
7
+ parseFrontmatter,
8
+ routeSlug,
9
+ scanMarkdownDocs,
10
+ type HumanAdapter,
11
+ } from './core.js'
12
+
13
+ type SidebarDocFilter = {
14
+ readonly ids: ReadonlySet<string>
15
+ readonly autogenDirs: readonly string[]
16
+ readonly enabled: boolean
17
+ }
18
+
19
+ const docusaurusFileSlug = (relPath: string): string => {
20
+ const parts = relPath.split('/')
21
+ const file = parts.at(-1) ?? ''
22
+ const base = file.replace(/\.mdx?$/, '')
23
+ const parent = parts.at(-2)
24
+ if (/^(index|readme)$/i.test(base) || base === parent) return parts.slice(0, -1).join('/')
25
+ return routeSlug(relPath)
26
+ }
27
+
28
+ const docusaurusSlug = (relPath: string, raw: string): string => {
29
+ const slug = parseFrontmatter(raw).slug
30
+ if (!slug) return docusaurusFileSlug(relPath)
31
+ return slug.replace(/^\.\//, '').replace(/^\//, '').replace(/\/$/, '')
32
+ }
33
+
34
+ const docusaurusSidebarId = (relPath: string, raw: string): string => {
35
+ const id = parseFrontmatter(raw).id
36
+ if (!id) return routeSlug(relPath)
37
+ const parts = relPath.split('/').slice(0, -1)
38
+ return [...parts, id].filter(Boolean).join('/')
39
+ }
40
+
41
+ const docusaurusRecordId = (relPath: string, raw: string): string => {
42
+ const frontmatter = parseFrontmatter(raw)
43
+ return frontmatter.package ?? frontmatter.module ?? docusaurusSidebarId(relPath, raw)
44
+ }
45
+
46
+ const sidebarsValue = (file: string): unknown => {
47
+ if (!existsSync(file)) return undefined
48
+ const raw = readFileSync(file, 'utf8')
49
+ .replace(/import\s+type\s+[\s\S]*?;?\n/g, '')
50
+ .replace(/:\s*[A-Za-z0-9_.$<>{}\[\],\s]+(?=\s*=)/g, '')
51
+ .replace(/\s+satisfies\s+[A-Za-z0-9_.$<>{}\[\],\s]+(?=\s*(?:;|\n|$))/g, '')
52
+ .replace(/export\s+default/, 'module.exports =')
53
+ const sandbox = { module: { exports: {} as unknown }, exports: {} }
54
+ vm.runInNewContext(raw, sandbox, { timeout: 250 })
55
+ return sandbox.module.exports
56
+ }
57
+
58
+ const visitSidebar = (value: unknown, filter: { ids: Set<string>; autogenDirs: string[] }): void => {
59
+ if (typeof value === 'string') {
60
+ filter.ids.add(value)
61
+ return
62
+ }
63
+ if (Array.isArray(value)) {
64
+ for (const item of value) visitSidebar(item, filter)
65
+ return
66
+ }
67
+ if (!value || typeof value !== 'object') return
68
+
69
+ const item = value as Record<string, unknown>
70
+ if ((item.type === 'doc' || item.type === 'ref') && typeof item.id === 'string') {
71
+ filter.ids.add(item.id)
72
+ }
73
+ if (item.type === 'autogenerated' && typeof item.dirName === 'string') {
74
+ filter.autogenDirs.push(item.dirName)
75
+ }
76
+ if (Array.isArray(item.items)) visitSidebar(item.items, filter)
77
+ if (item.link && typeof item.link === 'object') visitSidebar(item.link, filter)
78
+ for (const child of Object.values(item)) {
79
+ if (Array.isArray(child)) visitSidebar(child, filter)
80
+ }
81
+ }
82
+
83
+ const readSidebars = (root: string, sidebarsFile: string | undefined): SidebarDocFilter => {
84
+ if (!sidebarsFile) return { enabled: false, ids: new Set(), autogenDirs: [] }
85
+ const value = sidebarsValue(join(root, sidebarsFile))
86
+ const filter = { ids: new Set<string>(), autogenDirs: [] as string[] }
87
+ visitSidebar(value, filter)
88
+ return { enabled: true, ...filter }
89
+ }
90
+
91
+ const isIncludedBySidebar = (filter: SidebarDocFilter, id: string): boolean => {
92
+ if (!filter.enabled) return true
93
+ if (filter.ids.has(id)) return true
94
+ return filter.autogenDirs.some((dir) => {
95
+ const prefix = dir === '.' ? '' : dir.replace(/\/$/, '')
96
+ return prefix ? id === prefix || id.startsWith(`${prefix}/`) : true
97
+ })
98
+ }
99
+
100
+ export const docusaurusAdapter: HumanAdapter = {
101
+ plugin: 'docusaurus',
102
+ scan: ({ root, config }) => {
103
+ const docsDir = optionString(config.options, ['docsDir', 'root'])
104
+ if (!docsDir) return []
105
+ const sidebarFilter = readSidebars(root, optionString(config.options, ['sidebarsFile']))
106
+ return scanMarkdownDocs(root, docsDir, {
107
+ includeRelPath: (relPath, raw) => isIncludedBySidebar(sidebarFilter, docusaurusSidebarId(relPath, raw)),
108
+ idForDoc: docusaurusRecordId,
109
+ slugForDoc: docusaurusSlug,
110
+ urlPrefix: config.options?.urlPrefix,
111
+ })
112
+ },
113
+ }
@@ -0,0 +1,70 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ import { optionString, scanMarkdownDocs, type HumanAdapter } from './core.js'
5
+
6
+ type FumadocsMeta = {
7
+ readonly pages?: unknown
8
+ }
9
+
10
+ const readMetaPages = (dir: string): string[] | undefined => {
11
+ const file = join(dir, 'meta.json')
12
+ if (!existsSync(file)) return undefined
13
+ try {
14
+ const meta = JSON.parse(readFileSync(file, 'utf8')) as FumadocsMeta
15
+ return Array.isArray(meta.pages) ? meta.pages.filter((page): page is string => typeof page === 'string') : undefined
16
+ } catch {
17
+ return undefined
18
+ }
19
+ }
20
+
21
+ const pageKey = (part: string): string => part.replace(/\.mdx?$/, '')
22
+
23
+ const isDotFile = (relPath: string): boolean => {
24
+ const file = relPath.split('/').at(-1) ?? ''
25
+ return file.startsWith('.')
26
+ }
27
+
28
+ const isListedByMeta = (contentRoot: string, relPath: string): boolean => {
29
+ if (isDotFile(relPath)) return false
30
+
31
+ const parts = relPath.split('/')
32
+
33
+ for (let i = 0; i < parts.length; i += 1) {
34
+ const dir = join(contentRoot, ...parts.slice(0, i))
35
+ const pages = readMetaPages(dir)
36
+ if (!pages?.length || pages.includes('...')) continue
37
+
38
+ const key = pageKey(parts[i] ?? '')
39
+ if (!pages.includes(key) && !pages.includes(`./${key}`)) return false
40
+ }
41
+
42
+ return true
43
+ }
44
+
45
+ export const fumadocsAdapter: HumanAdapter = {
46
+ plugin: 'fumadocs',
47
+ scan: ({ root, config }) => {
48
+ const contentDir = optionString(config.options, ['contentDir', 'root'])
49
+ if (!contentDir) return []
50
+ const contentRoot = join(root, contentDir)
51
+ const excludePrefixes = Array.isArray(config.options?.excludePrefixes)
52
+ ? config.options.excludePrefixes.filter((v): v is string => typeof v === 'string')
53
+ : typeof config.options?.excludePrefix === 'string'
54
+ ? [config.options.excludePrefix]
55
+ : []
56
+ // Nested agent corpora under Fumadocs (e.g. content/docs/for-agents) should not be human docs
57
+ if (!excludePrefixes.includes('for-agents')) excludePrefixes.push('for-agents')
58
+
59
+ return scanMarkdownDocs(root, contentDir, {
60
+ includeRelPath: (relPath) => {
61
+ if (excludePrefixes.some((prefix) => relPath === prefix || relPath.startsWith(`${prefix}/`))) {
62
+ return false
63
+ }
64
+ return isListedByMeta(contentRoot, relPath)
65
+ },
66
+ urlPrefix: config.options?.urlPrefix,
67
+ stripGroups: true,
68
+ })
69
+ },
70
+ }
@@ -0,0 +1,52 @@
1
+ import type { DocBridgeConfigV1, HumanCorpusConfig } from '../../config/schema.js'
2
+ import { docusaurusAdapter } from './docusaurus.js'
3
+ import { fumadocsAdapter } from './fumadocs.js'
4
+ import type { HumanAdapter, HumanDocMap, HumanDocRecord } from './core.js'
5
+ import { plainMarkdownAdapter } from './plain-markdown.js'
6
+
7
+ export type { HumanAdapter, HumanDocMap, HumanDocRecord } from './core.js'
8
+
9
+ const ADAPTERS: readonly HumanAdapter[] = [
10
+ plainMarkdownAdapter,
11
+ fumadocsAdapter,
12
+ docusaurusAdapter,
13
+ ]
14
+
15
+ const humanConfigs = (config: DocBridgeConfigV1): HumanCorpusConfig[] => {
16
+ const human = config.corpus.human
17
+ if (!human) return []
18
+ return Array.isArray(human) ? human : [human]
19
+ }
20
+
21
+ export const scanHumanDocRecords = (
22
+ root: string,
23
+ config: DocBridgeConfigV1,
24
+ ): HumanDocRecord[] => {
25
+ const out: HumanDocRecord[] = []
26
+ const seen = new Set<string>()
27
+ const agentRoot = config.corpus.agent.root.replace(/\/$/, '')
28
+
29
+ for (const human of humanConfigs(config)) {
30
+ const adapter = ADAPTERS.find((candidate) => candidate.plugin === human.plugin)
31
+ if (!adapter) continue
32
+ for (const record of adapter.scan({ root, config: human })) {
33
+ // Never treat agent-corpus files as human docs (nested for-agents, etc.)
34
+ if (
35
+ record.path === agentRoot ||
36
+ record.path.startsWith(`${agentRoot}/`) ||
37
+ record.path.includes('/for-agents/') ||
38
+ record.path.endsWith('/for-agents')
39
+ ) {
40
+ continue
41
+ }
42
+ if (seen.has(record.id)) continue
43
+ seen.add(record.id)
44
+ out.push(record)
45
+ }
46
+ }
47
+
48
+ return out
49
+ }
50
+
51
+ export const scanHumanDocs = (root: string, config: DocBridgeConfigV1): HumanDocMap =>
52
+ Object.fromEntries(scanHumanDocRecords(root, config).map((doc) => [doc.id, doc.url]))
@@ -0,0 +1,10 @@
1
+ import { optionString, scanMarkdownDocs, type HumanAdapter } from './core.js'
2
+
3
+ export const plainMarkdownAdapter: HumanAdapter = {
4
+ plugin: 'plain-markdown',
5
+ scan: ({ root, config }) => {
6
+ const humanRoot =
7
+ optionString(config.options, ['contentDir', 'root', 'docsDir']) ?? 'docs'
8
+ return scanMarkdownDocs(root, humanRoot, { urlPrefix: config.options?.urlPrefix })
9
+ },
10
+ }
@@ -0,0 +1,19 @@
1
+ import type { DocBridgeConfigV1 } from '../config/schema.js'
2
+ import type { KnowledgeEntry } from '../schemas/doc-bridge-index.js'
3
+
4
+ export const renderLlmsTxt = (
5
+ config: DocBridgeConfigV1,
6
+ knowledge: readonly KnowledgeEntry[],
7
+ projectName: string,
8
+ ): string => {
9
+ const preamble =
10
+ config.index?.llmsTxt?.preamble ??
11
+ `# ${projectName}\n\n> Agent-readable documentation index generated by ak-docs (@agentskit/doc-bridge).\n`
12
+
13
+ const lines = knowledge.slice(0, 500).map((entry) => {
14
+ const desc = entry.description ? `: ${entry.description}` : ''
15
+ return `- [${entry.title}](${entry.path})${desc}`
16
+ })
17
+
18
+ return `${preamble.trim()}\n\n## Knowledge\n\n${lines.join('\n')}\n`
19
+ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ scanHumanDocRecords,
3
+ scanHumanDocs,
4
+ type HumanAdapter,
5
+ type HumanDocMap,
6
+ type HumanDocRecord,
7
+ } from '../human-adapters/index.js'
@@ -0,0 +1,72 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { basename, join } from 'node:path'
3
+
4
+ import type { DocBridgeConfigV1 } from '../../config/schema.js'
5
+ import { expandWorkspaceGlobs } from '../../lib/glob-expand.js'
6
+ import { toPosix } from '../../lib/paths.js'
7
+
8
+ export type DiscoveredPackage = {
9
+ readonly id: string
10
+ readonly path: string
11
+ readonly name?: string
12
+ }
13
+
14
+ const parsePnpmWorkspace = (yaml: string): string[] => {
15
+ const lines = yaml.split('\n')
16
+ const patterns: string[] = []
17
+ let inPackages = false
18
+ for (const line of lines) {
19
+ const trimmed = line.trim()
20
+ if (trimmed === 'packages:') {
21
+ inPackages = true
22
+ continue
23
+ }
24
+ if (inPackages) {
25
+ if (trimmed.startsWith('- ')) {
26
+ patterns.push(trimmed.slice(2).trim().replace(/^['"]|['"]$/g, ''))
27
+ continue
28
+ }
29
+ if (trimmed && !trimmed.startsWith('#')) inPackages = false
30
+ }
31
+ }
32
+ return patterns
33
+ }
34
+
35
+ const readPackageJson = (dir: string): { name?: string } | null => {
36
+ const file = join(dir, 'package.json')
37
+ if (!existsSync(file)) return null
38
+ try {
39
+ return JSON.parse(readFileSync(file, 'utf8')) as { name?: string }
40
+ } catch {
41
+ return null
42
+ }
43
+ }
44
+
45
+ export const discoverPnpmPackages = (
46
+ root: string,
47
+ config: DocBridgeConfigV1,
48
+ ): DiscoveredPackage[] => {
49
+ const explicit = config.routing?.options?.packages
50
+ let patterns = explicit
51
+ if (!patterns?.length) {
52
+ const workspaceFile = join(root, 'pnpm-workspace.yaml')
53
+ if (existsSync(workspaceFile)) {
54
+ patterns = parsePnpmWorkspace(readFileSync(workspaceFile, 'utf8'))
55
+ }
56
+ }
57
+ if (!patterns?.length) return []
58
+
59
+ const dirs = expandWorkspaceGlobs(root, patterns)
60
+ const out: DiscoveredPackage[] = []
61
+
62
+ for (const abs of dirs) {
63
+ const pkg = readPackageJson(abs)
64
+ if (!pkg) continue
65
+ const rel = toPosix(abs.replace(`${toPosix(root)}/`, ''))
66
+ const folderId = basename(abs)
67
+ const id = pkg.name?.startsWith('@') ? pkg.name.split('/').pop() ?? folderId : (pkg.name ?? folderId)
68
+ out.push({ id, path: rel, ...(pkg.name ? { name: pkg.name } : {}) })
69
+ }
70
+
71
+ return out.sort((a, b) => a.id.localeCompare(b.id))
72
+ }
@@ -0,0 +1,181 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ import type { DocBridgeConfigV1 } from '../config/schema.js'
5
+ import {
6
+ firstHeading,
7
+ firstParagraph,
8
+ frontmatterString,
9
+ frontmatterStringList,
10
+ parseFrontmatter,
11
+ slugFromPath,
12
+ type FrontmatterData,
13
+ } from '../lib/markdown.js'
14
+ import { toPosix } from '../lib/paths.js'
15
+ import { walkFiles } from '../lib/walk.js'
16
+ import type { KnowledgeEntry } from '../schemas/doc-bridge-index.js'
17
+
18
+ export type CorpusDoc = KnowledgeEntry & {
19
+ readonly absPath: string
20
+ readonly relPath: string
21
+ readonly frontmatter: FrontmatterData
22
+ }
23
+
24
+ export type OwnershipSeed = {
25
+ readonly id: string
26
+ readonly path: string
27
+ readonly purpose?: string
28
+ readonly checks?: readonly string[]
29
+ readonly agentDoc: string
30
+ readonly humanDoc?: string
31
+ }
32
+
33
+ const relFromRoot = (root: string, abs: string): string => toPosix(abs.replace(`${toPosix(root)}/`, ''))
34
+
35
+ export const scanAgentCorpus = (root: string, config: DocBridgeConfigV1): CorpusDoc[] => {
36
+ const agentRoot = join(root, config.corpus.agent.root)
37
+ const files = walkFiles(agentRoot, { extensions: ['.md', '.mdx'] })
38
+ const corpusRelRoot = toPosix(config.corpus.agent.root)
39
+
40
+ return files.map((abs) => {
41
+ const relToCorpus = toPosix(abs.replace(`${toPosix(agentRoot)}/`, ''))
42
+ const relPath = `${corpusRelRoot}/${relToCorpus}`
43
+ const raw = readFileSync(abs, 'utf8')
44
+ const { data: frontmatter } = parseFrontmatter(raw)
45
+ const id =
46
+ frontmatterString(frontmatter, 'id') ??
47
+ frontmatterString(frontmatter, 'package') ??
48
+ slugFromPath(relToCorpus)
49
+ const title = firstHeading(raw) ?? id
50
+ const description = firstParagraph(raw)
51
+ return {
52
+ id,
53
+ type: 'agent-doc',
54
+ title,
55
+ path: relPath,
56
+ absPath: abs,
57
+ relPath,
58
+ frontmatter,
59
+ ...(description ? { description } : {}),
60
+ }
61
+ })
62
+ }
63
+
64
+ export const guessAgentDocForPackage = (
65
+ corpus: readonly CorpusDoc[],
66
+ packageId: string,
67
+ ): string | undefined => {
68
+ const candidates = [
69
+ (doc: CorpusDoc) => frontmatterString(doc.frontmatter, 'package') === packageId,
70
+ (doc: CorpusDoc) => doc.id === packageId,
71
+ (doc: CorpusDoc) => doc.relPath.endsWith(`/packages/${packageId}.md`),
72
+ (doc: CorpusDoc) => doc.relPath.endsWith(`/packages/${packageId}.mdx`),
73
+ (doc: CorpusDoc) => doc.relPath.endsWith(`/packages/${packageId}/index.md`),
74
+ (doc: CorpusDoc) => doc.relPath.endsWith(`/packages/${packageId}/index.mdx`),
75
+ (doc: CorpusDoc) => doc.relPath.endsWith(`/${packageId}.md`),
76
+ (doc: CorpusDoc) => doc.relPath.endsWith(`/${packageId}.mdx`),
77
+ (doc: CorpusDoc) => doc.relPath.includes(`/packages/${packageId}/`),
78
+ ]
79
+ for (const match of candidates) {
80
+ const hit = corpus.find(match)
81
+ if (hit) return hit.path
82
+ }
83
+ return undefined
84
+ }
85
+
86
+ /** Ownership seeds from agent-doc frontmatter (`package` + `editRoot`). */
87
+ export const ownershipFromFrontmatter = (corpus: readonly CorpusDoc[]): readonly OwnershipSeed[] => {
88
+ const out: OwnershipSeed[] = []
89
+
90
+ for (const doc of corpus) {
91
+ const type = frontmatterString(doc.frontmatter, 'type')
92
+ const id =
93
+ frontmatterString(doc.frontmatter, 'package') ??
94
+ (type === 'package' || type === 'module' || type === 'pattern'
95
+ ? frontmatterString(doc.frontmatter, 'id') ?? doc.id
96
+ : undefined)
97
+ const path =
98
+ frontmatterString(doc.frontmatter, 'editRoot') ??
99
+ frontmatterString(doc.frontmatter, 'path') ??
100
+ (type === 'package' || type === 'module' ? `packages/${id}` : undefined)
101
+ if (!id || !path) continue
102
+ const purpose = frontmatterString(doc.frontmatter, 'purpose') ?? doc.description
103
+ const checks = frontmatterStringList(doc.frontmatter, 'checks')
104
+ const humanDoc = frontmatterString(doc.frontmatter, 'humanDoc')
105
+ out.push({
106
+ id,
107
+ path,
108
+ agentDoc: doc.path,
109
+ ...(purpose ? { purpose } : {}),
110
+ ...(checks ? { checks } : {}),
111
+ ...(humanDoc ? { humanDoc } : {}),
112
+ })
113
+ }
114
+
115
+ return out
116
+ }
117
+
118
+ /**
119
+ * Path-based ownership when frontmatter is incomplete:
120
+ * - packages/id.md(x) → package path packages/id
121
+ * - pillars/.../name-pattern.md → pattern ownership (edit root = file)
122
+ * - registry/id/README.md → registry agent
123
+ */
124
+ export const ownershipFromCorpus = (corpus: readonly CorpusDoc[]): readonly OwnershipSeed[] => {
125
+ const out: OwnershipSeed[] = []
126
+ const seen = new Set<string>()
127
+
128
+ for (const doc of corpus) {
129
+ // already handled if package+editRoot present
130
+ if (frontmatterString(doc.frontmatter, 'package') && frontmatterString(doc.frontmatter, 'editRoot')) {
131
+ continue
132
+ }
133
+
134
+ let id: string | undefined
135
+ let path: string | undefined
136
+ let purpose = frontmatterString(doc.frontmatter, 'purpose') ?? doc.description
137
+
138
+ const packagesFile = /(?:^|\/)packages\/([^/]+)\.(?:md|mdx)$/.exec(doc.relPath)
139
+ const packagesIndex = /(?:^|\/)packages\/([^/]+)\/index\.(?:md|mdx)$/.exec(doc.relPath)
140
+ const registryReadme = /(?:^|\/)registry\/([^/]+)\/README\.(?:md|mdx)$/i.exec(doc.relPath)
141
+ const patternFile = /(?:^|\/)pillars\/[^/]+\/([^/]+(?:-pattern)?)\.(?:md|mdx)$/.exec(doc.relPath)
142
+ const topLevelPkg = /(?:^|\/)for-agents\/([^/]+)\.(?:md|mdx)$/.exec(doc.relPath)
143
+
144
+ if (packagesFile?.[1] && packagesFile[1] !== 'index') {
145
+ id = packagesFile[1]
146
+ path = `packages/${id}`
147
+ } else if (packagesIndex?.[1]) {
148
+ id = packagesIndex[1]
149
+ path = `packages/${id}`
150
+ } else if (registryReadme?.[1]) {
151
+ id = registryReadme[1]
152
+ path = `registry/${id}`
153
+ } else if (patternFile?.[1] && patternFile[1] !== 'index' && patternFile[1] !== 'universal') {
154
+ id = patternFile[1]
155
+ path = doc.path
156
+ purpose = purpose ?? `Playbook pattern: ${id}`
157
+ } else if (topLevelPkg?.[1] && topLevelPkg[1] !== 'index' && topLevelPkg[1] !== 'INDEX') {
158
+ // agentskit for-agents/core.mdx style
159
+ id = topLevelPkg[1]
160
+ path = `packages/${id}`
161
+ }
162
+
163
+ if (!id || !path || seen.has(id)) continue
164
+ seen.add(id)
165
+
166
+ const humanDoc = frontmatterString(doc.frontmatter, 'humanDoc')
167
+ const checks = frontmatterStringList(doc.frontmatter, 'checks')
168
+ out.push({
169
+ id,
170
+ path,
171
+ agentDoc: doc.path,
172
+ ...(purpose ? { purpose } : {}),
173
+ ...(checks ? { checks } : {}),
174
+ ...(humanDoc ? { humanDoc } : {}),
175
+ })
176
+ }
177
+
178
+ return out
179
+ }
180
+
181
+ export { relFromRoot }