@agentskit/doc-bridge 0.1.0-alpha.3 → 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.
- package/CHANGELOG.md +46 -0
- package/README.md +139 -57
- package/action.yml +78 -0
- package/dist/cli/program.js +987 -69
- package/dist/cli/program.js.map +1 -1
- package/dist/index.d.ts +238 -26
- package/dist/index.js +704 -22
- package/dist/index.js.map +1 -1
- package/docs/DOGFOOD-ROUND3.md +74 -0
- package/docs/POSITIONING.md +2 -0
- package/docs/RELEASE.md +5 -3
- package/docs/chat-and-rag.md +2 -0
- package/docs/getting-started.md +14 -1
- package/docs/landing/index.html +299 -0
- package/docs/mcp.md +10 -1
- package/docs/ollama-demo.md +64 -0
- package/docs/playbook/doc-bridge-pattern.md +114 -0
- package/docs/recipes/index-pipeline.md +89 -0
- package/docs/skills/doc-bridge.md +64 -0
- package/docs/spec/cli.md +6 -0
- package/docs/spec/playbook-feedback.md +12 -4
- package/examples/demo-example/doc-bridge.config.json +23 -0
- package/examples/demo-example/docs/for-agents/INDEX.md +3 -0
- package/examples/demo-example/docs/for-agents/packages/example.md +14 -0
- package/examples/demo-example/package.json +7 -0
- package/examples/demo-example/src/.gitkeep +0 -0
- package/examples/demo-monorepo/doc-bridge.config.json +37 -0
- package/examples/demo-monorepo/docs/for-agents/INDEX.md +4 -0
- package/examples/demo-monorepo/docs/for-agents/packages/auth.md +22 -0
- package/examples/demo-monorepo/docs/for-agents/packages/billing.md +19 -0
- package/examples/demo-monorepo/docs/human/guides/auth.md +7 -0
- package/examples/demo-monorepo/package.json +7 -0
- package/examples/demo-monorepo/packages/auth/package.json +8 -0
- package/examples/demo-monorepo/packages/billing/package.json +7 -0
- package/examples/demo-monorepo/pnpm-workspace.yaml +2 -0
- package/examples/ollama-chat.config.ts +42 -0
- package/package.json +5 -2
- package/src/cli/demo.ts +143 -0
- package/src/cli/program.ts +194 -14
- package/src/doctor/badge.ts +53 -0
- package/src/doctor/run-doctor.ts +286 -0
- package/src/index-builder/build-handoffs.ts +19 -1
- package/src/index-builder/watch-index.ts +94 -0
- package/src/index.ts +24 -0
- package/src/mcp/install.ts +84 -0
- package/src/memory/github-pr.ts +190 -0
- package/src/playbook/doc-bridge-pattern.ts +121 -0
- package/src/query/query.ts +19 -1
- package/src/schemas/agent-handoff.ts +11 -0
- package/src/version.ts +1 -1
|
@@ -183,6 +183,18 @@ export const buildLookup = (
|
|
|
183
183
|
}
|
|
184
184
|
ownership[pkg.id] = record
|
|
185
185
|
|
|
186
|
+
const bridge = record.humanDoc
|
|
187
|
+
? /^https?:\/\//.test(record.humanDoc)
|
|
188
|
+
? { humanDoc: 'external' as const }
|
|
189
|
+
: { humanDoc: 'linked' as const }
|
|
190
|
+
: config.corpus.human
|
|
191
|
+
? {
|
|
192
|
+
humanDoc: 'missing' as const,
|
|
193
|
+
action: 'ak-docs bootstrap agent-docs',
|
|
194
|
+
bootstrap: `docs/for-agents/human/${pkg.id}.md`,
|
|
195
|
+
}
|
|
196
|
+
: undefined
|
|
197
|
+
|
|
186
198
|
handoffs[pkg.id] = {
|
|
187
199
|
type: 'agent-handoff',
|
|
188
200
|
schemaVersion: 1,
|
|
@@ -201,7 +213,13 @@ export const buildLookup = (
|
|
|
201
213
|
editRoots: [record.path],
|
|
202
214
|
checks: [...record.checks],
|
|
203
215
|
...(record.humanDoc ? { humanDoc: record.humanDoc } : {}),
|
|
204
|
-
|
|
216
|
+
...(bridge ? { bridge } : {}),
|
|
217
|
+
notes: [
|
|
218
|
+
...(record.purpose ? [record.purpose] : []),
|
|
219
|
+
...(!record.humanDoc && config.corpus.human
|
|
220
|
+
? [`Human guide missing for ${pkg.id}. Run: ak-docs bootstrap agent-docs`]
|
|
221
|
+
: []),
|
|
222
|
+
],
|
|
205
223
|
}
|
|
206
224
|
}
|
|
207
225
|
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, watch } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { DocBridgeConfigV1 } from '../config/schema.js'
|
|
5
|
+
import { buildDocBridgeIndex } from './build-index.js'
|
|
6
|
+
|
|
7
|
+
export type WatchIndexOptions = {
|
|
8
|
+
readonly root: string
|
|
9
|
+
readonly config: DocBridgeConfigV1
|
|
10
|
+
readonly configPath?: string
|
|
11
|
+
readonly debounceMs?: number
|
|
12
|
+
readonly onRebuild?: (summary: { knowledgeCount: number; handoffCount: number; hash: string }) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const WATCH_PATTERN = /\.(md|mdx|json|ya?ml|mdc)$/i
|
|
16
|
+
|
|
17
|
+
const collectWatchRoots = (root: string, config: DocBridgeConfigV1, configPath?: string): string[] => {
|
|
18
|
+
const roots = new Set<string>()
|
|
19
|
+
roots.add(resolve(root, config.corpus.agent.root))
|
|
20
|
+
const humanSources = config.corpus.human
|
|
21
|
+
? Array.isArray(config.corpus.human)
|
|
22
|
+
? config.corpus.human
|
|
23
|
+
: [config.corpus.human]
|
|
24
|
+
: []
|
|
25
|
+
for (const source of humanSources) {
|
|
26
|
+
const humanOpts = source.options ?? {}
|
|
27
|
+
for (const key of ['contentDir', 'docsDir', 'root']) {
|
|
28
|
+
const value = humanOpts[key]
|
|
29
|
+
if (typeof value === 'string' && value.length) roots.add(resolve(root, value))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (configPath) roots.add(dirname(resolve(configPath)))
|
|
33
|
+
return [...roots].filter((dir) => existsSync(dir))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const watchDocBridgeIndex = (opts: WatchIndexOptions): Promise<number> => {
|
|
37
|
+
const debounceMs = opts.debounceMs ?? 350
|
|
38
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
39
|
+
let running = false
|
|
40
|
+
|
|
41
|
+
const rebuild = (): void => {
|
|
42
|
+
if (timer) clearTimeout(timer)
|
|
43
|
+
timer = setTimeout(() => {
|
|
44
|
+
if (running) return
|
|
45
|
+
running = true
|
|
46
|
+
try {
|
|
47
|
+
const result = buildDocBridgeIndex({ root: opts.root, config: opts.config })
|
|
48
|
+
const handoffCount = Object.keys(result.index.handoffs ?? {}).length
|
|
49
|
+
const summary = {
|
|
50
|
+
knowledgeCount: result.index.knowledge.length,
|
|
51
|
+
handoffCount,
|
|
52
|
+
hash: result.index.contentHash.slice(0, 8),
|
|
53
|
+
}
|
|
54
|
+
opts.onRebuild?.(summary)
|
|
55
|
+
process.stdout.write(
|
|
56
|
+
`[ak-docs] indexed ${summary.knowledgeCount} docs, ${summary.handoffCount} handoffs (${summary.hash}…)\n`,
|
|
57
|
+
)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
`[ak-docs] index failed: ${error instanceof Error ? error.message : String(error)}\n`,
|
|
61
|
+
)
|
|
62
|
+
} finally {
|
|
63
|
+
running = false
|
|
64
|
+
}
|
|
65
|
+
}, debounceMs)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
rebuild()
|
|
69
|
+
|
|
70
|
+
for (const dir of collectWatchRoots(opts.root, opts.config, opts.configPath)) {
|
|
71
|
+
watch(dir, { recursive: true }, (_event, filename) => {
|
|
72
|
+
if (!filename || !WATCH_PATTERN.test(filename)) return
|
|
73
|
+
rebuild()
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const configDir = resolve(opts.root)
|
|
78
|
+
if (existsSync(configDir)) {
|
|
79
|
+
watch(configDir, (_event, filename) => {
|
|
80
|
+
if (!filename || !/doc-bridge\.config/.test(filename)) return
|
|
81
|
+
rebuild()
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
process.stdout.write(
|
|
86
|
+
`[ak-docs] watching ${collectWatchRoots(opts.root, opts.config, opts.configPath).join(', ') || opts.root} (Ctrl+C to stop)\n`,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return new Promise((resolvePromise) => {
|
|
90
|
+
const onSignal = () => resolvePromise(0)
|
|
91
|
+
process.once('SIGINT', onSignal)
|
|
92
|
+
process.once('SIGTERM', onSignal)
|
|
93
|
+
})
|
|
94
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,9 @@ export {
|
|
|
20
20
|
HandoffTargetTypeSchema,
|
|
21
21
|
HANDOFF_SCHEMA_VERSION,
|
|
22
22
|
normalizeAgentHandoff,
|
|
23
|
+
HandoffBridgeSchema,
|
|
23
24
|
type AgentHandoffV1,
|
|
25
|
+
type HandoffBridge,
|
|
24
26
|
type AgentSearchV1,
|
|
25
27
|
type HandoffTarget,
|
|
26
28
|
type HandoffTargetType,
|
|
@@ -74,6 +76,22 @@ export {
|
|
|
74
76
|
type GateRunResult,
|
|
75
77
|
} from './gates/run-gates.js'
|
|
76
78
|
export { MCP_TOOLS, handleMcpRequest, startMcpStdioServer } from './mcp/server.js'
|
|
79
|
+
export { installMcpConfig, mcpSnippet, type McpInstallResult, type McpInstallTarget } from './mcp/install.js'
|
|
80
|
+
export { runDoctor, formatDoctorText, type DoctorReport, type DoctorIssue, type DoctorCoverage } from './doctor/run-doctor.js'
|
|
81
|
+
export {
|
|
82
|
+
doctorBadgeMetrics,
|
|
83
|
+
formatDoctorBadgeJson,
|
|
84
|
+
formatDoctorBadgeMarkdown,
|
|
85
|
+
type DoctorBadgeMetrics,
|
|
86
|
+
} from './doctor/badge.js'
|
|
87
|
+
export { watchDocBridgeIndex, type WatchIndexOptions } from './index-builder/watch-index.js'
|
|
88
|
+
export {
|
|
89
|
+
promoteMemoryToGithubPr,
|
|
90
|
+
writePromotionDraft,
|
|
91
|
+
defaultPromotionDraftPath,
|
|
92
|
+
type GithubPrOptions,
|
|
93
|
+
type GithubPrResult,
|
|
94
|
+
} from './memory/github-pr.js'
|
|
77
95
|
export { sha256NormalizedV1 } from './index-builder/content-hash.js'
|
|
78
96
|
export { IndexNotFoundError, indexFilePath, loadDocBridgeIndex, resolveRoot } from './query/load-index.js'
|
|
79
97
|
export { runQuery, type QueryKind, type QueryRequest, type QueryResult } from './query/query.js'
|
|
@@ -118,3 +136,9 @@ export { projectRootFromConfigPath } from './config/load-config.js'
|
|
|
118
136
|
export { createDocBridgeRag } from './intelligence/rag.js'
|
|
119
137
|
export { runChatOnce, startInkChat } from './intelligence/chat.js'
|
|
120
138
|
export { PeerMissingError, layer1InstallHint } from './intelligence/peers.js'
|
|
139
|
+
export {
|
|
140
|
+
DOC_BRIDGE_PATTERN_ID,
|
|
141
|
+
DOC_BRIDGE_PATTERN_META,
|
|
142
|
+
docBridgePatternMarkdown,
|
|
143
|
+
docBridgePatternPayload,
|
|
144
|
+
} from './playbook/doc-bridge-pattern.js'
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { dirname, join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export type McpInstallTarget = 'cursor' | 'claude'
|
|
6
|
+
|
|
7
|
+
export type McpInstallResult = {
|
|
8
|
+
readonly ok: boolean
|
|
9
|
+
readonly target: McpInstallTarget
|
|
10
|
+
readonly configPath: string
|
|
11
|
+
readonly created: boolean
|
|
12
|
+
readonly serverName: string
|
|
13
|
+
readonly nextSteps: readonly string[]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SERVER_NAME = 'ak-docs'
|
|
17
|
+
|
|
18
|
+
const mcpServerEntry = (root: string) => ({
|
|
19
|
+
command: 'npx',
|
|
20
|
+
args: ['ak-docs', 'mcp'],
|
|
21
|
+
cwd: root,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const readJson = (path: string): Record<string, unknown> => {
|
|
25
|
+
if (!existsSync(path)) return {}
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown
|
|
28
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
29
|
+
? (parsed as Record<string, unknown>)
|
|
30
|
+
: {}
|
|
31
|
+
} catch {
|
|
32
|
+
return {}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const writeJson = (path: string, value: Record<string, unknown>): void => {
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
38
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolveTargetPath = (target: McpInstallTarget, root: string): string => {
|
|
42
|
+
if (target === 'cursor') return resolve(root, '.cursor', 'mcp.json')
|
|
43
|
+
return join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const installMcpConfig = (
|
|
47
|
+
root: string,
|
|
48
|
+
target: McpInstallTarget,
|
|
49
|
+
): McpInstallResult => {
|
|
50
|
+
const configPath = resolveTargetPath(target, root)
|
|
51
|
+
const created = !existsSync(configPath)
|
|
52
|
+
const existing = readJson(configPath)
|
|
53
|
+
const servers =
|
|
54
|
+
existing.mcpServers && typeof existing.mcpServers === 'object' && !Array.isArray(existing.mcpServers)
|
|
55
|
+
? { ...(existing.mcpServers as Record<string, unknown>) }
|
|
56
|
+
: {}
|
|
57
|
+
|
|
58
|
+
servers[SERVER_NAME] = mcpServerEntry(root)
|
|
59
|
+
writeJson(configPath, { ...existing, mcpServers: servers })
|
|
60
|
+
|
|
61
|
+
const nextSteps =
|
|
62
|
+
target === 'cursor'
|
|
63
|
+
? [
|
|
64
|
+
'Restart Cursor or reload MCP servers',
|
|
65
|
+
'Paste the doc-bridge skill into Cursor rules (see docs/skills/doc-bridge.md)',
|
|
66
|
+
'Before editing packages/* call handoff.resolve',
|
|
67
|
+
]
|
|
68
|
+
: [
|
|
69
|
+
'Restart Claude Desktop',
|
|
70
|
+
'Run ak-docs index after doc changes',
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
ok: true,
|
|
75
|
+
target,
|
|
76
|
+
configPath,
|
|
77
|
+
created,
|
|
78
|
+
serverName: SERVER_NAME,
|
|
79
|
+
nextSteps,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export const mcpSnippet = (root: string): string =>
|
|
84
|
+
JSON.stringify({ mcpServers: { [SERVER_NAME]: mcpServerEntry(root) } }, null, 2)
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { execFileSync, spawnSync } from 'node:child_process'
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import type { MemoryPromotionDraft } from './pipeline.js'
|
|
6
|
+
|
|
7
|
+
export type GithubPrOptions = {
|
|
8
|
+
readonly dryRun?: boolean
|
|
9
|
+
readonly force?: boolean
|
|
10
|
+
readonly branch?: string
|
|
11
|
+
readonly base?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type GithubPrResult = {
|
|
15
|
+
readonly ok: boolean
|
|
16
|
+
readonly dryRun: boolean
|
|
17
|
+
readonly draftPath: string
|
|
18
|
+
readonly branch: string
|
|
19
|
+
readonly prUrl?: string
|
|
20
|
+
readonly previewUrl?: string
|
|
21
|
+
readonly commands: readonly string[]
|
|
22
|
+
readonly message: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const run = (cmd: string, args: readonly string[], cwd: string): { ok: boolean; out: string } => {
|
|
26
|
+
const result = spawnSync(cmd, [...args], { cwd, encoding: 'utf8' })
|
|
27
|
+
const out = `${result.stdout ?? ''}${result.stderr ?? ''}`.trim()
|
|
28
|
+
return { ok: result.status === 0, out }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const hasGh = (): boolean => run('gh', ['--version'], process.cwd()).ok
|
|
32
|
+
|
|
33
|
+
const slug = (): string => new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
|
|
34
|
+
|
|
35
|
+
export const defaultPromotionDraftPath = (root: string): string =>
|
|
36
|
+
join(root, '.doc-bridge', 'drafts', `memory-promotion-${slug()}.md`)
|
|
37
|
+
|
|
38
|
+
export const writePromotionDraft = (root: string, draft: MemoryPromotionDraft, path?: string): string => {
|
|
39
|
+
const draftPath = path ?? defaultPromotionDraftPath(root)
|
|
40
|
+
mkdirSync(join(root, '.doc-bridge', 'drafts'), { recursive: true })
|
|
41
|
+
writeFileSync(draftPath, `${draft.body}\n`, 'utf8')
|
|
42
|
+
return draftPath
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const promoteMemoryToGithubPr = (
|
|
46
|
+
root: string,
|
|
47
|
+
draft: MemoryPromotionDraft,
|
|
48
|
+
options: GithubPrOptions = {},
|
|
49
|
+
): GithubPrResult => {
|
|
50
|
+
if (!draft.ok && !options.force) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
dryRun: Boolean(options.dryRun),
|
|
54
|
+
draftPath: '',
|
|
55
|
+
branch: '',
|
|
56
|
+
commands: [],
|
|
57
|
+
message: 'Safety scan blocked promotion. Fix findings or pass --force to draft anyway.',
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const branch = options.branch ?? `doc-bridge/memory-promotion-${slug()}`
|
|
62
|
+
const draftPath = writePromotionDraft(root, draft)
|
|
63
|
+
const relDraft = draftPath.startsWith(`${root}/`) ? draftPath.slice(root.length + 1) : draftPath
|
|
64
|
+
|
|
65
|
+
const commands = [
|
|
66
|
+
`git checkout -b ${branch}`,
|
|
67
|
+
`git add ${relDraft}`,
|
|
68
|
+
`git commit -m "draft: doc-bridge memory promotion"`,
|
|
69
|
+
`git push -u origin ${branch}`,
|
|
70
|
+
`gh pr create --draft --title "${draft.title}" --body-file ${relDraft}${options.base ? ` --base ${options.base}` : ''}`,
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
if (options.dryRun) {
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
dryRun: true,
|
|
77
|
+
draftPath,
|
|
78
|
+
branch,
|
|
79
|
+
commands,
|
|
80
|
+
message: `Wrote draft to ${relDraft}. Run the printed git/gh commands to open a draft PR.`,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!existsSync(join(root, '.git'))) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
dryRun: false,
|
|
88
|
+
draftPath,
|
|
89
|
+
branch,
|
|
90
|
+
commands,
|
|
91
|
+
message: 'Not a git repository. Commit the draft manually or run with --dry-run.',
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!hasGh()) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
dryRun: false,
|
|
99
|
+
draftPath,
|
|
100
|
+
branch,
|
|
101
|
+
commands,
|
|
102
|
+
message: 'GitHub CLI (gh) not found. Install gh or use --dry-run for local draft + commands.',
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const auth = run('gh', ['auth', 'status'], root)
|
|
107
|
+
if (!auth.ok) {
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
dryRun: false,
|
|
111
|
+
draftPath,
|
|
112
|
+
branch,
|
|
113
|
+
commands,
|
|
114
|
+
message: `gh is not authenticated. Run: gh auth login\n${auth.out}`,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const checkout = run('git', ['checkout', '-b', branch], root)
|
|
119
|
+
if (!checkout.ok) {
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
dryRun: false,
|
|
123
|
+
draftPath,
|
|
124
|
+
branch,
|
|
125
|
+
commands,
|
|
126
|
+
message: `git checkout failed: ${checkout.out}`,
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
run('git', ['add', relDraft], root)
|
|
131
|
+
const commit = run('git', ['commit', '-m', 'draft: doc-bridge memory promotion'], root)
|
|
132
|
+
if (!commit.ok) {
|
|
133
|
+
return {
|
|
134
|
+
ok: false,
|
|
135
|
+
dryRun: false,
|
|
136
|
+
draftPath,
|
|
137
|
+
branch,
|
|
138
|
+
commands,
|
|
139
|
+
message: `git commit failed: ${commit.out}`,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const push = run('git', ['push', '-u', 'origin', branch], root)
|
|
144
|
+
if (!push.ok) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
dryRun: false,
|
|
148
|
+
draftPath,
|
|
149
|
+
branch,
|
|
150
|
+
commands,
|
|
151
|
+
message: `git push failed: ${push.out}`,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const prArgs = [
|
|
156
|
+
'pr',
|
|
157
|
+
'create',
|
|
158
|
+
'--draft',
|
|
159
|
+
'--title',
|
|
160
|
+
draft.title,
|
|
161
|
+
'--body-file',
|
|
162
|
+
relDraft,
|
|
163
|
+
...(options.base ? ['--base', options.base] : []),
|
|
164
|
+
]
|
|
165
|
+
let prUrl = ''
|
|
166
|
+
try {
|
|
167
|
+
prUrl = execFileSync('gh', prArgs, { cwd: root, encoding: 'utf8' }).trim()
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
dryRun: false,
|
|
173
|
+
draftPath,
|
|
174
|
+
branch,
|
|
175
|
+
commands,
|
|
176
|
+
message: `gh pr create failed: ${message}`,
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
dryRun: false,
|
|
183
|
+
draftPath,
|
|
184
|
+
branch,
|
|
185
|
+
prUrl,
|
|
186
|
+
...(prUrl ? { previewUrl: prUrl } : {}),
|
|
187
|
+
commands,
|
|
188
|
+
message: prUrl ? `Draft PR opened: ${prUrl}` : 'Draft PR created.',
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export const DOC_BRIDGE_PATTERN_ID = 'doc-bridge-pattern' as const
|
|
2
|
+
|
|
3
|
+
export const DOC_BRIDGE_PATTERN_META = {
|
|
4
|
+
id: DOC_BRIDGE_PATTERN_ID,
|
|
5
|
+
title: 'Doc Bridge Pattern',
|
|
6
|
+
slug: 'pillars/ai-collaboration/doc-bridge-pattern',
|
|
7
|
+
license: 'CC-BY-4.0',
|
|
8
|
+
visibility: 'public',
|
|
9
|
+
playbookUrl: 'https://playbook.agentskit.io/patterns/doc-bridge-pattern',
|
|
10
|
+
npmPackage: '@agentskit/doc-bridge',
|
|
11
|
+
cli: 'ak-docs',
|
|
12
|
+
} as const
|
|
13
|
+
|
|
14
|
+
export const docBridgePatternMarkdown = (): string => `---
|
|
15
|
+
type: pattern
|
|
16
|
+
id: ${DOC_BRIDGE_PATTERN_ID}
|
|
17
|
+
purpose: Route coding agents to the correct package, checks, and human docs in any monorepo.
|
|
18
|
+
owner: AgentsKit
|
|
19
|
+
license: CC-BY-4.0
|
|
20
|
+
visibility: public
|
|
21
|
+
tags: [agents, documentation, monorepo, handoff, mcp]
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Doc Bridge Pattern
|
|
25
|
+
|
|
26
|
+
**AgentHandoff for your monorepo** — deterministic routing so agents edit the right roots, run the right checks, and stay linked to human documentation.
|
|
27
|
+
|
|
28
|
+
## Problem
|
|
29
|
+
|
|
30
|
+
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*.
|
|
31
|
+
|
|
32
|
+
## Solution (three artifacts)
|
|
33
|
+
|
|
34
|
+
| Artifact | Role |
|
|
35
|
+
|----------|------|
|
|
36
|
+
| **AgentHandoff** | JSON handoff: \`startHere\`, \`editRoots\`, \`checks\`, \`humanDoc\`, \`bridge\` |
|
|
37
|
+
| **DocBridgeIndex** | Deterministic index + \`contentHash\` for CI freshness gates |
|
|
38
|
+
| **Self-describe** | \`llms.txt\`, \`capabilities.json\` for discovery |
|
|
39
|
+
|
|
40
|
+
## Four loops
|
|
41
|
+
|
|
42
|
+
| Loop | Command | Outcome |
|
|
43
|
+
|------|---------|---------|
|
|
44
|
+
| **Act** | \`ak-docs query package <id> --agent\` | Agent edits only \`editRoots\` |
|
|
45
|
+
| **Bridge** | \`ak-docs bootstrap agent-docs\` | Link agent corpus ↔ human site |
|
|
46
|
+
| **Learn** | \`ak-docs memory promote --pr\` | HITL draft PR from agent memory |
|
|
47
|
+
| **Explain** | \`ak-docs ask "<question>"\` | Local consult + handoff preview |
|
|
48
|
+
|
|
49
|
+
## 60-second proof
|
|
50
|
+
|
|
51
|
+
\`\`\`bash
|
|
52
|
+
npm i -D @agentskit/doc-bridge
|
|
53
|
+
npx ak-docs demo --text
|
|
54
|
+
ak-docs mcp install --cursor
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
## AgentHandoff example
|
|
58
|
+
|
|
59
|
+
\`\`\`json
|
|
60
|
+
{
|
|
61
|
+
"type": "agent-handoff",
|
|
62
|
+
"startHere": "docs/for-agents/packages/auth.md",
|
|
63
|
+
"editRoots": ["packages/auth"],
|
|
64
|
+
"checks": ["pnpm --filter @demo/auth test"],
|
|
65
|
+
"humanDoc": "/docs/guides/auth",
|
|
66
|
+
"bridge": { "humanDoc": "linked" }
|
|
67
|
+
}
|
|
68
|
+
\`\`\`
|
|
69
|
+
|
|
70
|
+
When \`humanDoc\` is missing, handoffs surface \`bridge.action: "ak-docs bootstrap agent-docs"\` — a feature, not a silent gap.
|
|
71
|
+
|
|
72
|
+
## MCP contract
|
|
73
|
+
|
|
74
|
+
Agents call \`handoff.resolve\` before editing \`packages/*\`:
|
|
75
|
+
|
|
76
|
+
1. Read \`startHere\`
|
|
77
|
+
2. Stay inside \`editRoots\`
|
|
78
|
+
3. Run every \`checks\` command
|
|
79
|
+
4. Link \`humanDoc\` or escalate missing bridge
|
|
80
|
+
|
|
81
|
+
## CI gate
|
|
82
|
+
|
|
83
|
+
\`\`\`yaml
|
|
84
|
+
- uses: AgentsKit-io/doc-bridge@v1.0.0
|
|
85
|
+
\`\`\`
|
|
86
|
+
|
|
87
|
+
Or: \`ak-docs index && ak-docs gate run\` — stale index fails the PR.
|
|
88
|
+
|
|
89
|
+
## Coverage metric
|
|
90
|
+
|
|
91
|
+
\`\`\`bash
|
|
92
|
+
ak-docs doctor --text
|
|
93
|
+
ak-docs doctor --badge
|
|
94
|
+
\`\`\`
|
|
95
|
+
|
|
96
|
+
Teams track handoff % and human-bridge % daily.
|
|
97
|
+
|
|
98
|
+
## When to use
|
|
99
|
+
|
|
100
|
+
- pnpm/npm monorepos with real package ownership
|
|
101
|
+
- Fumadocus / Docusaurus human sites + dense agent corpus
|
|
102
|
+
- Cursor / Claude / Codex agents that should not guess edit roots
|
|
103
|
+
|
|
104
|
+
## When not to use
|
|
105
|
+
|
|
106
|
+
- Single-file repos with no ownership boundaries (AGENTS.md may suffice)
|
|
107
|
+
- Hosted doc chat as primary product (doc-bridge is routing + bridge, not SaaS chat)
|
|
108
|
+
|
|
109
|
+
## References
|
|
110
|
+
|
|
111
|
+
- npm: https://www.npmjs.com/package/@agentskit/doc-bridge
|
|
112
|
+
- repo: https://github.com/AgentsKit-io/doc-bridge
|
|
113
|
+
- skill: https://github.com/AgentsKit-io/doc-bridge/blob/master/docs/skills/doc-bridge.md
|
|
114
|
+
- landing: https://agentskit-io.github.io/doc-bridge/
|
|
115
|
+
`
|
|
116
|
+
|
|
117
|
+
export const docBridgePatternPayload = () => ({
|
|
118
|
+
...DOC_BRIDGE_PATTERN_META,
|
|
119
|
+
format: 'okf-pattern-v1',
|
|
120
|
+
body: docBridgePatternMarkdown(),
|
|
121
|
+
})
|
package/src/query/query.ts
CHANGED
|
@@ -32,6 +32,18 @@ const handoffForPackage = (
|
|
|
32
32
|
const owner = index.lookup?.ownership?.[id]
|
|
33
33
|
if (!owner) throw new Error(`Unknown package/ownership id "${id}". Try: ak-docs list packages`)
|
|
34
34
|
|
|
35
|
+
const bridge = owner.humanDoc
|
|
36
|
+
? /^https?:\/\//.test(owner.humanDoc)
|
|
37
|
+
? { humanDoc: 'external' as const }
|
|
38
|
+
: { humanDoc: 'linked' as const }
|
|
39
|
+
: config.corpus.human
|
|
40
|
+
? {
|
|
41
|
+
humanDoc: 'missing' as const,
|
|
42
|
+
action: 'ak-docs bootstrap agent-docs',
|
|
43
|
+
bootstrap: `docs/for-agents/human/${id}.md`,
|
|
44
|
+
}
|
|
45
|
+
: undefined
|
|
46
|
+
|
|
35
47
|
return normalizeAgentHandoff({
|
|
36
48
|
type: 'agent-handoff',
|
|
37
49
|
source: config.index?.outFile ?? '.doc-bridge/index.json',
|
|
@@ -47,7 +59,13 @@ const handoffForPackage = (
|
|
|
47
59
|
editRoots: [owner.path],
|
|
48
60
|
checks: [...owner.checks],
|
|
49
61
|
...(owner.humanDoc ? { humanDoc: owner.humanDoc } : {}),
|
|
50
|
-
|
|
62
|
+
...(bridge ? { bridge } : {}),
|
|
63
|
+
notes: [
|
|
64
|
+
...(owner.purpose ? [owner.purpose] : []),
|
|
65
|
+
...(!owner.humanDoc && config.corpus.human
|
|
66
|
+
? [`Human guide missing for ${id}. Run: ak-docs bootstrap agent-docs`]
|
|
67
|
+
: []),
|
|
68
|
+
],
|
|
51
69
|
})
|
|
52
70
|
}
|
|
53
71
|
|
|
@@ -28,6 +28,16 @@ export const HandoffTargetSchema = z
|
|
|
28
28
|
|
|
29
29
|
export type HandoffTarget = z.infer<typeof HandoffTargetSchema>
|
|
30
30
|
|
|
31
|
+
export const HandoffBridgeSchema = z
|
|
32
|
+
.object({
|
|
33
|
+
humanDoc: z.enum(['linked', 'missing', 'external']),
|
|
34
|
+
action: z.string().min(1).max(256).optional(),
|
|
35
|
+
bootstrap: z.string().min(1).max(256).optional(),
|
|
36
|
+
})
|
|
37
|
+
.strict()
|
|
38
|
+
|
|
39
|
+
export type HandoffBridge = z.infer<typeof HandoffBridgeSchema>
|
|
40
|
+
|
|
31
41
|
/** v1 — canonical AgentHandoff. Legacy payloads may omit schemaVersion. */
|
|
32
42
|
export const AgentHandoffV1Schema = z
|
|
33
43
|
.object({
|
|
@@ -40,6 +50,7 @@ export const AgentHandoffV1Schema = z
|
|
|
40
50
|
editRoots: z.array(z.string().min(1).max(512)).max(32),
|
|
41
51
|
checks: z.array(z.string().min(1).max(256)).max(32),
|
|
42
52
|
humanDoc: z.string().min(1).max(512).nullable().optional(),
|
|
53
|
+
bridge: HandoffBridgeSchema.optional(),
|
|
43
54
|
playbookPatterns: z.array(z.string().url()).max(16).optional(),
|
|
44
55
|
notes: z.array(z.string().min(1).max(1024)).max(16),
|
|
45
56
|
})
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '
|
|
1
|
+
export const PACKAGE_VERSION = '1.0.0'
|