diff --git a/.changeset/codebase-wiki-starter-pack.md b/.changeset/codebase-wiki-starter-pack.md new file mode 100644 index 000000000..bf9251225 --- /dev/null +++ b/.changeset/codebase-wiki-starter-pack.md @@ -0,0 +1,5 @@ +--- +"@inkeep/open-knowledge": minor +--- + +Add the `codebase-wiki` starter pack — an agent-authored, navigable wiki of your codebase. `ok seed --pack codebase-wiki` scaffolds a `wiki/` knowledge base (architecture, modules, flows, concepts, and guides — each with a page template) plus an `OVERVIEW` hub and a generation log. The new `workflow({ kind: "wiki" })` guide drives an agent to generate — and later incrementally refresh — the wiki from source, with mermaid diagrams, cross-linked pages, and source references, tuned by two natural-language knobs (`audience`: internal/public, `depth`: tour/standard/exhaustive). Version-controlled and private by default; the wiki doubles as durable grounding context for future agent sessions. diff --git a/packages/app/src/components/PackCardGrid.tsx b/packages/app/src/components/PackCardGrid.tsx index 326c85b35..912300720 100644 --- a/packages/app/src/components/PackCardGrid.tsx +++ b/packages/app/src/components/PackCardGrid.tsx @@ -2,6 +2,7 @@ import { Trans, useLingui } from '@lingui/react/macro'; import { ArrowRight, + BookMarked, Compass, FileCheck, GitBranch, @@ -19,6 +20,7 @@ import { cn } from '@/lib/utils'; const PACK_ICONS: Record> = { 'knowledge-base': Library, 'software-lifecycle': GitBranch, + 'codebase-wiki': BookMarked, 'plain-notes': StickyNote, worldbuilding: Compass, 'writing-pipeline': PenLine, diff --git a/packages/app/src/lib/desktop-bridge-types.ts b/packages/app/src/lib/desktop-bridge-types.ts index 940e2ac5c..b71e44d2f 100644 --- a/packages/app/src/lib/desktop-bridge-types.ts +++ b/packages/app/src/lib/desktop-bridge-types.ts @@ -51,6 +51,7 @@ export interface OkSeedError { export type OkPackId = | 'knowledge-base' | 'software-lifecycle' + | 'codebase-wiki' | 'plain-notes' | 'worldbuilding' | 'writing-pipeline' diff --git a/packages/core/src/desktop-bridge.ts b/packages/core/src/desktop-bridge.ts index 59d030786..04e4345d7 100644 --- a/packages/core/src/desktop-bridge.ts +++ b/packages/core/src/desktop-bridge.ts @@ -267,6 +267,7 @@ interface OkSeedError { type OkPackId = | 'knowledge-base' | 'software-lifecycle' + | 'codebase-wiki' | 'plain-notes' | 'worldbuilding' | 'writing-pipeline' diff --git a/packages/server/assets/skills/packs/codebase-wiki/SKILL.md b/packages/server/assets/skills/packs/codebase-wiki/SKILL.md new file mode 100644 index 000000000..621556af5 --- /dev/null +++ b/packages/server/assets/skills/packs/codebase-wiki/SKILL.md @@ -0,0 +1,83 @@ +--- +name: open-knowledge-pack-codebase-wiki +description: "How to work in a Codebase Wiki project (the `codebase-wiki` starter pack): an agent-authored, source-grounded wiki of the surrounding codebase. Read when the project has a `wiki/` knowledge base with `architecture/`, `modules/`, `flows/`, `concepts/`, and `guides/` sections plus `wiki/OVERVIEW.md`. Carries the per-folder rules and freshness + log discipline, summarizes the audience/depth knobs and source-reference convention, and points to the `workflow({ kind: 'wiki' })` guide for the full generate/refresh procedure. Complements the platform `open-knowledge` skill; does not replace it." +compatibility: "Claude Code, Claude Desktop, Claude Cowork, Claude.ai web. Requires Open Knowledge MCP server. Installed project-local by `ok seed --pack codebase-wiki`." +metadata: + pack: "codebase-wiki" + author: "Inkeep" + repository: "https://github.com/inkeep/open-knowledge" +--- +# Codebase Wiki pack — how to work here + +This project holds an **agent-authored wiki of a codebase** — DeepWiki, but living in the repo. A coding agent reads the source and writes a navigable, diagram-rich, source-grounded wiki as markdown under `wiki/`. It is version-controlled and diffable, private by default, human+agent co-editable, renders in OK's live preview, and doubles as durable grounding context for future agent sessions. There is no separate Q&A surface — Q&A is "the OK-grounded agent + `search`". + +> This is pack guidance. The platform `open-knowledge` skill still governs every markdown operation (read/write/preview/linking/grounding). This layers the wiki workflow on top. + +## The shape + +``` +wiki/ + OVERVIEW.md hub: what it is, a big-picture architecture diagram, a nav map to every section. + Frontmatter carries `profile` (audience/depth) + `source_commit` (freshness anchor). + log.md append-only generation / refresh audit trail + architecture/ system boundaries, layers, subsystems, cross-cutting concerns + diagrams + modules/ one page per package / module: purpose, entry points, key files, deps + flows/ key end-to-end flows as sequence / flow diagrams + narrative + concepts/ glossary: atomic pages for domain terms / core abstractions + guides/ task-oriented "how / where do I change X" (filled at depth >= standard) +``` + +## Generating + refreshing + +Don't free-hand it — call **`workflow({ kind: "wiki" })`** and follow the phased, STOP-gated guide. It auto-detects mode: a stubbed `OVERVIEW.md` (empty `source_commit`) → **generate** (survey → overview → architecture → modules → flows → concepts → link-graph audit); a stamped `source_commit` → **refresh** (diff `source_commit..HEAD`, update only affected pages, re-stamp). + +**Two toolsets.** Read source code with NATIVE tools (`Read`/`Grep`/`Glob`/`Bash`) — OK MCP does not index non-markdown source. Author and audit the wiki with OK MCP verbs (`write`/`edit` for pages, `links`/`search` for the graph). Never hand-write wiki markdown with native `Write`/`Edit`. + +## The two knobs + +Two natural-language knobs, read from the user's request (e.g. "build the wiki, public and exhaustive") and recorded in `OVERVIEW.md` frontmatter (`profile: /`) so refreshes stay consistent: + +- **`audience`** — `internal` (default) or `public`. `public` means polished prose, no secrets / internal infra / ticket numbers, and GitHub-URL source references. +- **`depth`** — `tour` | `standard` (default) | `exhaustive`. Scales coverage from OVERVIEW + architecture + top flows up through per-package module pages, concepts, and task guides. + +The `workflow({ kind: "wiki" })` guide is the authoritative source for exactly how each knob shapes the output — invoke it before generating. + +## Source-reference convention + +- **Intra-wiki navigation** → OK doc links — they build the backlink / hub / orphan graph, so link liberally; density is how the wiki stays navigable. +- **Code references** → relative links + symbol code-spans (`internal`) or GitHub blob URLs (`public`). Source-file links produce **no dead-link noise** (the graph only tracks `.md`/`.mdx` edges). Never invent paths — reference only files you actually read. + +The full rules — the GitHub-URL / relative fallback, the `#Lxx` caveat, and the exact code-span shape — live in the `workflow({ kind: "wiki" })` guide. + +## Per-folder rules + +**`architecture/`** — One page per architectural area (boundaries, layers, subsystems, cross-cutting concerns). Each: a `mermaid` system-context or component diagram, key components (with source refs), and the design decisions behind them. Uses the `architecture-page` template. At `depth: tour`, modules fold in here. + +**`modules/`** — One page per package / module: purpose, responsibilities, public API / entry points, key files (linked per the convention), dependencies, and flows it participates in. Uses the `module-page` template. Skipped at `tour`; sub-module depth scales with the knob. + +**`flows/`** — Key end-to-end sequences as `mermaid` sequence / flow diagrams + narrative. Uses the `flow-page` template; add a **Failure modes** section at `exhaustive`. Link every module and concept the flow crosses. + +**`concepts/`** — Atomic glossary pages (one term each): definition, why it matters, where it lives in the code. Uses the `concept-page` template. Keep small and densely cross-linked so each concept becomes a hub. + +**`guides/`** — Task-oriented "how / where do I change X" walkthroughs: goal, steps, relevant code, gotchas. Uses the `guide-page` template. Populated at `standard`, rich at `exhaustive`, thin/empty at `tour`. + +## Freshness discipline (MUST) + +`OVERVIEW.md` frontmatter carries `source_commit` — the git HEAD the wiki was last generated/refreshed against. It is the freshness anchor: refresh mode diffs `source_commit..HEAD` to update only the affected pages, then re-stamps it. **Always re-stamp `source_commit` after a generate or refresh run** — a stale anchor silently breaks incremental refresh. + +## Log discipline (MUST) + +`wiki/log.md` is an append-only audit trail. **Append one dated entry per generation or refresh run** — one per run, not per page. Reference touched pages as markdown links (`[Server](./modules/server.md)`) so they register in the backlink graph. Entry shape: + +```markdown +## YYYY-MM-DD: + +- Profile: / +- source_commit: (was on refresh) +- Coverage: +- Pages: [Overview](./OVERVIEW.md), [Server](./modules/server.md), ... +``` + +## Templates + +Each folder ships a starter template (`architecture-page`, `module-page`, `flow-page`, `concept-page`, `guide-page`). Create with `write({ document: { path, template: "" } })`. Templates carry only structure (headings + frontmatter scaffold); what each section is for is described above and in the `workflow({ kind: "wiki" })` guide, not repeated inside document bodies. diff --git a/packages/server/assets/skills/project/SKILL.md b/packages/server/assets/skills/project/SKILL.md index 7e68288d9..c505c96f2 100644 --- a/packages/server/assets/skills/project/SKILL.md +++ b/packages/server/assets/skills/project/SKILL.md @@ -489,6 +489,7 @@ Typical day-2 flow: user shares a URL → `ingest` (preserve) → user asks "now - `knowledge-base` — source-grounded research articles - `software-lifecycle` — proposals, decisions, specs +- `codebase-wiki` — agent-authored wiki of your codebase - `plain-notes` — notes + daily journal - `worldbuilding` — fiction story wiki - `writing-pipeline` — drafts → published diff --git a/packages/server/scripts/build-skill-bundles.test.ts b/packages/server/scripts/build-skill-bundles.test.ts index 3ae5d6894..e34b8a641 100644 --- a/packages/server/scripts/build-skill-bundles.test.ts +++ b/packages/server/scripts/build-skill-bundles.test.ts @@ -123,11 +123,13 @@ describe('buildPackSkills', () => { expect(buildPackSkills(fixture({ discovery: '# d\n', project: '# p\n' }))).toEqual([]); }); - test('repo assets — all six starter packs are present to build', () => { + test('repo assets — all eight starter packs are present to build', () => { const packsDir = join(defaultPaths().skillsDir, 'packs'); const expected = [ + 'codebase-wiki', 'entity-vault', 'knowledge-base', + 'okf', 'plain-notes', 'software-lifecycle', 'worldbuilding', diff --git a/packages/server/src/mcp/tools/discover-body.ts b/packages/server/src/mcp/tools/discover-body.ts index f6afb4195..d95b59d3f 100644 --- a/packages/server/src/mcp/tools/discover-body.ts +++ b/packages/server/src/mcp/tools/discover-body.ts @@ -163,7 +163,7 @@ Six sub-passes, each with its own STOP gate. ### 5b. Hub identification (⛔ STOP gate 5b) -1. Run \`links({ kind: "hubs" })\` — surfaces existing high-fan-out docs. +1. Run \`links({ kind: "hubs" })\` — surfaces the most-linked-to docs (highest *inbound* links), i.e. pages already acting as hubs. 2. For each substantial \`[KB]\` folder (those that got templates in Phase 4): - Check if a hub already exists (\`/README.md\`, \`/INDEX.md\`, \`/CATALOGUE.md\`). - If yes: don't create a new one (anti-pattern: don't create INDEX.md hubs). diff --git a/packages/server/src/mcp/tools/wiki-body.ts b/packages/server/src/mcp/tools/wiki-body.ts new file mode 100644 index 000000000..c09d8fcb1 --- /dev/null +++ b/packages/server/src/mcp/tools/wiki-body.ts @@ -0,0 +1,162 @@ +export function buildWikiBody(contentDir: string): string { + return `# Codebase Wiki — Generate + Refresh + +You're invoked because the user wants a **wiki of this codebase** — DeepWiki, but living in their own repo. Your job: read the source code and author a navigable, diagram-rich, source-grounded wiki as markdown INTO the OK knowledge base, under \`wiki/\`. It is version-controlled, private by default, co-editable, renders in OK's live preview, and becomes grounding context for future agent sessions. + +Content directory: \`${contentDir}\` (from \`.ok/config.yml\`). The wiki lives at \`${contentDir}/wiki/\`. + +**Two toolsets, two jobs.** Read source code with your NATIVE tools (\`Read\`/\`Grep\`/\`Glob\`/\`Bash\` — OK MCP does not index non-markdown source). Author and audit the wiki with OK MCP verbs (\`write\`/\`edit\` for pages, \`links\`/\`search\` for the graph, \`exec\` for markdown listings). Never hand-write the wiki markdown with native \`Write\`/\`Edit\` — that bypasses attribution, backlinks, and the live preview. + +**Server requirement.** Phases that author or audit the wiki (\`write\`/\`edit\`/\`links\`/\`search\`) need the OK Hocuspocus server. Source reading (native tools) and git (\`exec\`/native \`Bash\`) do not. If a \`write\` returns "Hocuspocus server is not running", tell the user to run \`ok start\` and retry — do NOT fall back to native file writes for the wiki markdown. + +**Prerequisite.** This guide assumes the \`codebase-wiki\` pack is seeded (\`ok seed --pack codebase-wiki\` → \`wiki/\` with \`architecture/ modules/ flows/ concepts/ guides/\`, each carrying folder frontmatter + a page template, plus \`wiki/OVERVIEW.md\` + \`wiki/log.md\`). If \`exec("ls -A ${contentDir}/wiki")\` shows the layout is missing, tell the user to seed first, then re-invoke. + +--- + +## The two knobs + +Read these from the user's natural-language request (e.g. "build the wiki, public and exhaustive"). They are NOT engine config — they tune what you produce. Record the chosen profile in \`wiki/OVERVIEW.md\` frontmatter (\`profile: /\`) so refreshes stay consistent. + +| Knob | Values | Effect | +|---|---|---| +| \`audience\` | \`internal\` (default) │ \`public\` | \`internal\`: dense; may cite internal infra; reference source via **relative links + symbol code-spans**. \`public\`: polished prose; **omit secrets, internal infra, ticket/PR numbers**; reference source via **GitHub blob URLs** (only if a remote exists — else fall back to relative). | +| \`depth\` | \`tour\` │ \`standard\` (default) │ \`exhaustive\` | \`tour\`: OVERVIEW + architecture + top flows only (fold modules into architecture). \`standard\`: + one page per package/module + concepts. \`exhaustive\`: + sub-module pages, \`guides/\`, per-flow failure modes, denser diagrams. | + +Default when the user says nothing: \`internal\` / \`standard\`. + +--- + +## Source-reference convention (how wiki pages point at code) + +- **Intra-wiki navigation** → OK doc links (\`[Auth flow](../flows/auth.md)\`). These build the full backlink / hub / orphan graph. Link liberally — density is how the wiki stays navigable. +- **Code references, \`internal\`** → a relative markdown link to the source file plus an inline code-span for the symbol — e.g. the \`bootServer()\` symbol in [boot.ts](../../packages/server/src/boot.ts). Relative links click-open in the asset preview and produce **no dead-link noise** (the link graph only tracks \`.md\`/\`.mdx\` edges, so source links are never reported dead). A cosmetic \`#Lxx\` is fine but not navigable. +- **Code references, \`public\`** → GitHub blob URLs (\`https://github.com///blob//path/to/file.ts\`) so a reader without the repo can follow them. Detect the remote with \`exec\` (or native \`git remote get-url origin\`); if there is none, fall back to relative links. + +Never invent paths — every source reference must point at a file you actually read. + +--- + +## Mode detection — generate vs. refresh + +\`exec("cat ${contentDir}/wiki/OVERVIEW.md")\` and read its frontmatter: + +- **\`source_commit\` is empty / OVERVIEW is the seeded stub** → **GENERATE** (Phases 0–7 below). +- **\`source_commit\` is stamped with a commit** → **REFRESH** (jump to the *Refresh mode* section). Reuse the recorded \`profile\` unless the user explicitly changes a knob. + +--- + +## GENERATE — phased, STOP-gated (⛔ = wait for user confirmation) + +Work the phases in order. Do not skip or batch ahead of a ⛔ gate. Each page is authored with OK \`write\`/\`edit\`; create from the seeded templates (\`write({ document: { path, template: "" } })\`) so pages start with the right skeleton, then fill the sections. + +### Phase 0 — Resolve profile + scope (⛔ STOP gate 0) + +1. Resolve \`audience\` + \`depth\` from the request (default \`internal/standard\`). +2. Propose the **coverage set** (which packages/dirs the wiki will document) and **exclusions** (vendored deps, build output, generated code, fixtures — \`node_modules/\`, \`dist/\`, \`build/\`, \`vendor/\`, \`third_party/\`, \`coverage/\`, lockfiles). +3. Present profile + coverage + exclusions to the user. **Wait for confirmation.** + +### Phase 1 — Survey (no writes) (⛔ STOP gate 1) + +Read, don't write. Use native tools: + +1. **Detect the stack** — languages, build system, package manager, workspace/monorepo layout, entry points. (\`Glob\` for manifests: \`package.json\`, \`Cargo.toml\`, \`go.mod\`, \`pyproject.toml\`, \`*.csproj\`; read them.) +2. **Bootstrap from existing docs** — READ \`README\`, \`AGENTS.md\`/\`CLAUDE.md\`, \`ARCHITECTURE.md\`, \`CONTRIBUTING\`, per-package READMEs. **Summarize and link to them; do NOT duplicate.** They are your highest-signal starting point. +3. **Map the modules** — list packages/top-level source dirs; note each one's rough purpose and entry points. +4. Produce an **inventory + a proposed page list scaled to \`depth\`**: + - \`tour\` → OVERVIEW + 1–3 architecture pages + top 1–3 flows. + - \`standard\` → + one module page per package + a concepts glossary. + - \`exhaustive\` → + sub-module pages, \`guides/\`, per-flow failure modes. +5. Present the page list. **Wait for confirmation.** + +### Phase 2 — Overview hub + +Author \`wiki/OVERVIEW.md\` (edit the seeded stub in place): +1. **What this is** — 1–2 paragraphs: what the project does, who it's for. +2. **Architecture at a glance** — a big-picture \`mermaid\` diagram. Example: + \`\`\`mermaid + flowchart TD + CLI[ok CLI] --> Server[Hocuspocus server] + App[React editor] --> Server + Server --> Disk[(markdown + .ok/)] + \`\`\` + (Mermaid label text has sharp edges — if a write returns a \`mermaid-parse-error\` warning, fix that fence and re-edit. \`palette({ components: ["Mermaid"] })\` lists the gotchas.) +3. **Navigation** — a map that links **every section and every page you will create**, as OK doc links. OVERVIEW is the hub; everything must be reachable from it. +4. **Stamp frontmatter**: set \`profile: /\` and \`source_commit: \` (run \`exec("git rev-parse HEAD")\` or native \`git\`). \`source_commit\` is the freshness anchor refresh mode diffs against — get it right. + +### Phase 3 — Architecture pages + +One page per architectural area (boundaries, layers, subsystems, cross-cutting concerns). Each: a system-context or component \`mermaid\` diagram, the key components (with source references per the convention), and the design decisions behind them. Cross-link the modules and flows each area touches. (At \`depth: tour\`, modules fold into these pages.) + +### Phase 4 — Module pages + +One page per package/module (skip at \`tour\`; scale sub-module depth by knob). Use the \`module-page\` template: purpose, responsibilities, public API / entry points, **key files** (linked per the convention), dependencies, and the flows it participates in. Cross-link concepts and flows. + +### Phase 5 — Flows + +Key end-to-end sequences (request lifecycle, build/deploy, a core interaction) as \`mermaid\` sequence or flow diagrams + narrative. At \`exhaustive\`, add a **Failure modes** section per flow. Link every module and concept the flow crosses. + +### Phase 6 — Concepts / glossary + +Atomic pages for domain terms and core abstractions (one term each): definition, why it matters, where it lives in the code. Keep them small and densely cross-linked so each concept becomes a hub for everywhere it appears. Link concepts from the architecture/module/flow pages that use them. + +### Phase 7 — Link-graph activation + audit + +1. Confirm **OVERVIEW links every page** (it is the nav hub). Add any missing nav links. This is OVERVIEW's check — it's a *source* (high out-degree), verified by forward links here, not by the \`hubs\` view below. +2. Run \`links({ kind: ["orphans", "hubs", "dead"] })\`: + - **orphans** — pages nothing links to. Adopt each by linking it from OVERVIEW or a relevant section page (or, rarely, justify it as intentionally standalone). + - **hubs** — ranks by *inbound* links. Confirm your **concept/module pages** show up here — that's the signal cross-linking actually happened. Don't expect \`OVERVIEW\`: a freshly authored nav page has almost no inbound links, so it won't appear (and that's correct — its coverage was checked in step 1). + - **dead** — fix or remove every dead link. (Source-file links never appear here — only \`.md\`/\`.mdx\` edges are tracked.) +3. Append a \`wiki/log.md\` entry (see *Log discipline*). +4. Tell the user the wiki is ready and surface the OVERVIEW preview URL. + +--- + +## REFRESH mode (re-invoke after code changes) + +Incremental by default — don't re-read the whole repo. + +1. Read \`source_commit\` + \`profile\` from \`wiki/OVERVIEW.md\` frontmatter. Reuse the profile unless the user changed a knob. +2. \`exec("git diff --stat ..HEAD")\` (or native \`git\`). Inspect the changed paths. +3. **Map changed code → affected pages.** For each changed package/area, update its module/architecture/flow pages (read the changed source first). Update OVERVIEW only if the structure changed (new package, removed subsystem, new top-level flow). +4. **Full-regen fallback.** If the diff is large or structural (many packages, a restructure), or git is unavailable / \`source_commit\` is missing, fall back to a full GENERATE pass rather than a partial patch. +5. **Re-stamp** \`source_commit\` to the new \`git rev-parse HEAD\` and re-run the Phase 7 link-graph audit on the touched pages. +6. Append a \`wiki/log.md\` refresh entry. + +--- + +## Log discipline + +\`wiki/log.md\` is an append-only audit trail. Append one dated entry per generation or refresh run (one per run, not per page). Reference touched pages as markdown links so they register in the backlink graph. Entry shape: + +\`\`\`markdown +## YYYY-MM-DD: + +- Profile: / +- source_commit: (was on refresh) +- Coverage: +- Pages: [Overview](./OVERVIEW.md), [Server](./modules/server.md), ... +\`\`\` + +--- + +## STOP rules / anti-patterns (load-bearing) + +- **Author wiki markdown only through OK \`write\`/\`edit\`** — never native \`Write\`/\`Edit\` (loses attribution, backlinks, live preview). Read source with native tools; write the wiki with OK. +- **Summarize existing docs, don't duplicate them** — link to \`README\`/\`ARCHITECTURE.md\`; the wiki adds navigation and synthesis, not a copy. +- **Every source reference points at a file you actually read** — no invented paths or symbols. +- **Respect the profile** — at \`audience: public\`, never write secrets, internal infra, or ticket/PR numbers; reference code via GitHub URLs (or relative if no remote). +- **OVERVIEW is the hub** — every page must be reachable from it. Don't leave orphans (Phase 7 catches them). +- **Don't scaffold folders by hand** — the \`codebase-wiki\` pack already created \`wiki/\` with templates; if it's missing, seed first. +- **Scale to \`depth\`** — don't write \`guides/\` or per-flow failure modes at \`tour\`; don't fold modules into architecture at \`exhaustive\`. +- **Refresh is incremental** — diff \`source_commit..HEAD\` and touch only affected pages; full-regen only on large/structural diffs or when git is unavailable. + +--- + +## Exit conditions + +- **Pack not seeded** — \`wiki/\` layout missing → tell the user to run \`ok seed --pack codebase-wiki\`, then re-invoke. Exit. +- **Server down** — a \`write\`/\`links\` call reports the server is not running → tell the user to run \`ok start\` and retry. Exit cleanly; re-invoking resumes (already-written pages persist). +- **User aborts at a ⛔ gate** → exit cleanly, leaving any already-written pages in place. +- **Empty / unreadable repo** (no source detected in Phase 1) → tell the user there's nothing to document yet. Exit. +`; +} diff --git a/packages/server/src/mcp/tools/workflow.test.ts b/packages/server/src/mcp/tools/workflow.test.ts index 961ad08b4..7208373ff 100644 --- a/packages/server/src/mcp/tools/workflow.test.ts +++ b/packages/server/src/mcp/tools/workflow.test.ts @@ -40,6 +40,7 @@ describe('workflow — kind discriminator + per-kind teaching errors', () => { expect(capture(cwd).name).toBe('workflow'); expect(DESCRIPTION).toContain('ingest'); expect(DESCRIPTION).toContain('discover'); + expect(DESCRIPTION).toContain('wiki'); }); test('kind:ingest with source returns the framed ingest plan', async () => { @@ -67,6 +68,14 @@ describe('workflow — kind discriminator + per-kind teaching errors', () => { expect(r.structuredContent?.previewUrl).toBeNull(); }); + test('kind:wiki returns the Codebase Wiki plan interpolated with contentDir', async () => { + const r = await capture(cwd).handler({ kind: 'wiki' }); + expect(r.isError).toBeFalsy(); + expect(textOf(r)).toContain('# Codebase Wiki'); + expect(textOf(r)).toContain('wiki/OVERVIEW.md'); + expect(r.structuredContent?.previewUrl).toBeNull(); + }); + test('kind:ingest without source returns a teaching error', async () => { const r = await capture(cwd).handler({ kind: 'ingest' }); expect(r.isError).toBe(true); diff --git a/packages/server/src/mcp/tools/workflow.ts b/packages/server/src/mcp/tools/workflow.ts index f181671d0..1b140c178 100644 --- a/packages/server/src/mcp/tools/workflow.ts +++ b/packages/server/src/mcp/tools/workflow.ts @@ -13,6 +13,7 @@ import { textPlusStructured, textResult, } from './shared.ts'; +import { buildWikiBody } from './wiki-body.ts'; export const DESCRIPTION = [ 'Procedural guides for the three-layer wiki workflow + brownfield onboarding. Returns a numbered plan (instructional text, not data) — you execute it. Dispatches on `kind`:', @@ -21,9 +22,10 @@ export const DESCRIPTION = [ '- `kind: "research"` — gather sources and write provisional findings for a question. Requires `topic`.', '- `kind: "consolidate"` — fold provisional material into a canonical article. Requires `topic`.', '- `kind: "discover"` — extract conventions from an existing repo (folder frontmatter + templates + link graph). No payload.', + '- `kind: "wiki"` — generate (or refresh) a navigable, diagram-rich, source-grounded wiki of this codebase into the `wiki/` knowledge base (the `codebase-wiki` pack). No payload; tune via natural-language `audience`/`depth` in your request.', '', '**Parameters:**', - '- `kind` — `ingest` | `research` | `consolidate` | `discover`.', + '- `kind` — `ingest` | `research` | `consolidate` | `discover` | `wiki`.', '- `source` — Required for `ingest`: the URL / file path / identifier to capture.', '- `topic` — Required for `research` / `consolidate`: the topic, question, or anchor URL.', '- `cwd` (optional) — Project root (see `cwd` description below).', @@ -40,7 +42,7 @@ export function register(server: ServerInstance, deps: WorkflowToolDeps): void { description: DESCRIPTION, inputSchema: { kind: z - .enum(['ingest', 'research', 'consolidate', 'discover']) + .enum(['ingest', 'research', 'consolidate', 'discover', 'wiki']) .describe('Which workflow guide to return.'), source: z .string() @@ -61,7 +63,7 @@ export function register(server: ServerInstance, deps: WorkflowToolDeps): void { }), }, async (args: { - kind: 'ingest' | 'research' | 'consolidate' | 'discover'; + kind: 'ingest' | 'research' | 'consolidate' | 'discover' | 'wiki'; source?: string; topic?: string; cwd?: string; @@ -98,6 +100,13 @@ export function register(server: ServerInstance, deps: WorkflowToolDeps): void { previewUrl: null, }); } + case 'wiki': { + const context = await resolveProjectConfigContext(deps.resolveCwd, deps.config, args.cwd); + if (!context.ok) return textResult(`Error: ${context.error}`, true); + return textPlusStructured(buildWikiBody(context.config.content.dir), { + previewUrl: null, + }); + } default: return textResult('Error: unknown workflow kind.', true); } diff --git a/packages/server/src/seed/apply.test.ts b/packages/server/src/seed/apply.test.ts index 8f2146b33..9a2e67b77 100644 --- a/packages/server/src/seed/apply.test.ts +++ b/packages/server/src/seed/apply.test.ts @@ -157,3 +157,72 @@ describe('applySeed — nested .ok/ era', () => { expect(result.errors[0]?.error).toContain('No content template registered'); }); }); + +describe('applySeed — codebase-wiki nested folder paths + wiki/-prefixed rootFiles', () => { + let projectDir: string; + const WIKI_PACK = STARTER_PACKS['codebase-wiki']; + + beforeEach(async () => { + projectDir = await mkdtemp(join(tmpdir(), 'seed-apply-wiki-')); + mkdirSync(join(projectDir, '.ok'), { recursive: true }); + writeFileSync(join(projectDir, '.ok', 'config.yml'), '', 'utf-8'); + }); + + afterEach(async () => { + await rm(projectDir, { recursive: true, force: true }); + }); + + test('writes every nested wiki/
with .ok/frontmatter.yml + template and no errors', async () => { + const plan = await planSeed({ projectDir, packId: 'codebase-wiki' }); + const result = await applySeed(plan, { projectDir, packId: 'codebase-wiki' }); + + expect(result.errors).toEqual([]); + expect(result.applied).toBe(plan.created.length); + + for (const folder of WIKI_PACK.folders) { + expect(existsSync(join(projectDir, folder.path, '.ok', 'frontmatter.yml'))).toBe(true); + expect( + existsSync( + join(projectDir, folder.path, '.ok', 'templates', `${folder.starterTemplate}.md`), + ), + ).toBe(true); + } + }); + + test('nested frontmatter.yml resolves to the folder defaults (resolver tolerates the slash)', async () => { + const plan = await planSeed({ projectDir, packId: 'codebase-wiki' }); + await applySeed(plan, { projectDir, packId: 'codebase-wiki' }); + + for (const folder of WIKI_PACK.folders) { + const fm = readFileSync(join(projectDir, folder.path, '.ok', 'frontmatter.yml'), 'utf-8'); + expect(fm).toContain(folder.title); + for (const tag of folder.tags) expect(fm).toContain(`- ${tag}`); + } + }); + + test('template bodies resolve verbatim for nested folder paths', async () => { + const plan = await planSeed({ projectDir, packId: 'codebase-wiki' }); + await applySeed(plan, { projectDir, packId: 'codebase-wiki' }); + + for (const folder of WIKI_PACK.folders) { + const tpl = readFileSync( + join(projectDir, folder.path, '.ok', 'templates', `${folder.starterTemplate}.md`), + 'utf-8', + ); + expect(tpl).toBe(WIKI_PACK.templates[folder.starterTemplate]); + } + }); + + test('wiki/-prefixed rootFiles land at wiki/OVERVIEW.md + wiki/log.md with verbatim content', async () => { + const plan = await planSeed({ projectDir, packId: 'codebase-wiki' }); + const result = await applySeed(plan, { projectDir, packId: 'codebase-wiki' }); + + expect(result.errors).toEqual([]); + expect(readFileSync(join(projectDir, 'wiki', 'OVERVIEW.md'), 'utf-8')).toBe( + WIKI_PACK.rootFiles?.['wiki/OVERVIEW.md'], + ); + expect(readFileSync(join(projectDir, 'wiki', 'log.md'), 'utf-8')).toBe( + WIKI_PACK.rootFiles?.['wiki/log.md'], + ); + }); +}); diff --git a/packages/server/src/seed/apply.ts b/packages/server/src/seed/apply.ts index dffbbff3b..97bc4dc55 100644 --- a/packages/server/src/seed/apply.ts +++ b/packages/server/src/seed/apply.ts @@ -10,23 +10,23 @@ function resolveFileContent( templateId: string, pack: ReturnType, ): string | undefined { - const fmMatch = /^([^/]+)\/\.ok\/frontmatter\.yml$/.exec(templateId); + if (pack.rootFiles?.[templateId] !== undefined) { + return pack.rootFiles[templateId]; + } + + const fmMatch = /^(.+)\/\.ok\/frontmatter\.yml$/.exec(templateId); if (fmMatch) { const folder = pack.folders.find((f) => f.path === fmMatch[1]); if (!folder) return undefined; return buildStarterFolderFrontmatterYaml(folder); } - const tplMatch = /^([^/]+)\/\.ok\/templates\/([^/]+)\.md$/.exec(templateId); + const tplMatch = /^(.+)\/\.ok\/templates\/([^/]+)\.md$/.exec(templateId); if (tplMatch) { const templateName = tplMatch[2] ?? ''; return pack.templates[templateName]; } - if (!templateId.includes('/') && pack.rootFiles?.[templateId] !== undefined) { - return pack.rootFiles[templateId]; - } - return undefined; } diff --git a/packages/server/src/seed/install-pack-skill.test.ts b/packages/server/src/seed/install-pack-skill.test.ts index 48186b05b..0366011f9 100644 --- a/packages/server/src/seed/install-pack-skill.test.ts +++ b/packages/server/src/seed/install-pack-skill.test.ts @@ -37,6 +37,15 @@ describe('installPackSkill', () => { ]); }); + test('installs the codebase-wiki pack skill from the source assets', () => { + const proj = tmpProject(); + setUpEditor(proj, '.claude'); + expect(installPackSkill(proj, 'codebase-wiki')).toEqual(['Claude Code']); + expect( + existsSync(join(proj, '.claude', 'skills', 'open-knowledge-pack-codebase-wiki', 'SKILL.md')), + ).toBe(true); + }); + test('no-op when no editor is set up (no platform skill present)', () => { expect(installPackSkill(tmpProject(), 'knowledge-base')).toEqual([]); }); diff --git a/packages/server/src/seed/plan.test.ts b/packages/server/src/seed/plan.test.ts index cc19a63cd..febaa97bf 100644 --- a/packages/server/src/seed/plan.test.ts +++ b/packages/server/src/seed/plan.test.ts @@ -117,3 +117,40 @@ describe('planSeed — nested .ok/ era', () => { await expect(planSeed({ projectDir, rootDir: '../escape' })).rejects.toThrow(SeedRootDirError); }); }); + +describe('planSeed — codebase-wiki nested paths', () => { + let projectDir: string; + const WIKI_PACK = STARTER_PACKS['codebase-wiki']; + + beforeEach(async () => { + projectDir = await mkdtemp(join(tmpdir(), 'seed-plan-wiki-')); + mkdirSync(join(projectDir, '.ok'), { recursive: true }); + writeFileSync(join(projectDir, '.ok', 'config.yml'), '', 'utf-8'); + }); + + afterEach(async () => { + await rm(projectDir, { recursive: true, force: true }); + }); + + test('plans nested folder + .ok/frontmatter.yml + template entries with slash-bearing template ids', async () => { + const plan = await planSeed({ projectDir, packId: 'codebase-wiki' }); + const byPath = new Map(plan.created.map((e) => [e.path, e])); + + for (const folder of WIKI_PACK.folders) { + expect(byPath.has(folder.path)).toBe(true); // e.g. wiki/architecture + expect(byPath.get(`${folder.path}/.ok/frontmatter.yml`)?.template).toBe( + `${folder.path}/.ok/frontmatter.yml`, + ); + expect( + byPath.get(`${folder.path}/.ok/templates/${folder.starterTemplate}.md`)?.template, + ).toBe(`${folder.path}/.ok/templates/${folder.starterTemplate}.md`); + } + }); + + test('plans wiki/-prefixed rootFiles at their nested paths', async () => { + const plan = await planSeed({ projectDir, packId: 'codebase-wiki' }); + const createdPaths = new Set(plan.created.map((e) => e.path)); + expect(createdPaths.has('wiki/OVERVIEW.md')).toBe(true); + expect(createdPaths.has('wiki/log.md')).toBe(true); + }); +}); diff --git a/packages/server/src/seed/starter.test.ts b/packages/server/src/seed/starter.test.ts index 485f68c8b..e7cae29c0 100644 --- a/packages/server/src/seed/starter.test.ts +++ b/packages/server/src/seed/starter.test.ts @@ -15,6 +15,7 @@ const STARTER_TEMPLATES = KNOWLEDGE_BASE_PACK.templates; const LOG_MD_TEMPLATE = KNOWLEDGE_BASE_PACK.rootFiles?.['log.md']; if (!LOG_MD_TEMPLATE) throw new Error('knowledge-base pack is missing log.md'); const ENTITY_VAULT_PACK = STARTER_PACKS['entity-vault']; +const CODEBASE_WIKI_PACK = STARTER_PACKS['codebase-wiki']; function stripTemplateMetadata(body: string): string { return parseTemplateFile(body).starterContent; @@ -136,9 +137,10 @@ describe('STARTER_FOLDER_FRONTMATTER_FILENAME', () => { }); describe('STARTER_PACKS — all packs structural validation', () => { - test('STARTER_PACK_IDS contains exactly the 7 expected packs (pinned to detect silent additions/deletions)', () => { - expect(STARTER_PACK_IDS.length).toBe(7); + test('STARTER_PACK_IDS contains exactly the 8 expected packs (pinned to detect silent additions/deletions)', () => { + expect(STARTER_PACK_IDS.length).toBe(8); expect([...STARTER_PACK_IDS].sort()).toEqual([ + 'codebase-wiki', 'entity-vault', 'knowledge-base', 'okf', @@ -192,10 +194,15 @@ describe('STARTER_PACKS — all packs structural validation', () => { } }); - test('every folder path uses kebab-case (matches existing scaffolder validator)', () => { + test('every folder path uses kebab-case per segment (nested paths allowed, e.g. wiki/architecture)', () => { for (const pack of Object.values(STARTER_PACKS)) { for (const folder of pack.folders) { - expect(folder.path).toMatch(/^[a-z][a-z0-9-]*$/); + for (const segment of folder.path.split('/')) { + expect( + segment, + `folder "${folder.path}" in pack "${pack.id}" has a non-kebab segment "${segment}"`, + ).toMatch(/^[a-z][a-z0-9-]*$/); + } } } }); @@ -286,23 +293,28 @@ describe('STARTER_PACKS — all packs structural validation', () => { } }); - test('every rootFile filename is safe (no path separators, no leading dot, non-empty)', () => { + test('every rootFile key is a safe relative path (forward-slash nesting allowed, no escape)', () => { for (const pack of Object.values(STARTER_PACKS)) { for (const filename of Object.keys(pack.rootFiles ?? {})) { - expect(filename.length, `Pack "${pack.id}" has an empty rootFile filename`).toBeGreaterThan( - 0, - ); - expect( - filename, - `Pack "${pack.id}" rootFile "${filename}" contains path separator`, - ).not.toContain('/'); + expect(filename.length, `Pack "${pack.id}" has an empty rootFile key`).toBeGreaterThan(0); expect( filename, `Pack "${pack.id}" rootFile "${filename}" contains backslash`, ).not.toContain('\\'); - expect(filename, `Pack "${pack.id}" rootFile "${filename}" is a dotfile`).not.toMatch( - /^\./, - ); + expect(filename, `Pack "${pack.id}" rootFile "${filename}" is absolute`).not.toMatch(/^\//); + for (const segment of filename.split('/')) { + expect( + segment.length, + `Pack "${pack.id}" rootFile "${filename}" has an empty path segment`, + ).toBeGreaterThan(0); + expect(segment, `Pack "${pack.id}" rootFile "${filename}" has a '..' segment`).not.toBe( + '..', + ); + expect( + segment, + `Pack "${pack.id}" rootFile "${filename}" has a dotfile segment "${segment}"`, + ).not.toMatch(/^\./); + } } } }); @@ -361,6 +373,59 @@ describe('Entity vault pack — GBrain-compatible Markdown shape', () => { }); }); +describe('Codebase wiki pack — nested wiki/ layout', () => { + test('scaffolds the five sections nested under wiki/, in reading order', () => { + expect(CODEBASE_WIKI_PACK.id).toBe('codebase-wiki'); + expect(CODEBASE_WIKI_PACK.folders.map((f) => f.path)).toEqual([ + 'wiki/architecture', + 'wiki/modules', + 'wiki/flows', + 'wiki/concepts', + 'wiki/guides', + ]); + }); + + test('every folder path nests under wiki/ so it scaffolds without --root', () => { + for (const folder of CODEBASE_WIKI_PACK.folders) { + expect( + folder.path.startsWith('wiki/'), + `folder "${folder.path}" should nest under wiki/`, + ).toBe(true); + } + }); + + test('no defaultSubfolder — the nested paths already place everything under wiki/', () => { + expect(CODEBASE_WIKI_PACK.defaultSubfolder).toBeUndefined(); + }); + + test('each section ships its named page template, resolvable in pack.templates', () => { + const expected: Record = { + 'wiki/architecture': 'architecture-page', + 'wiki/modules': 'module-page', + 'wiki/flows': 'flow-page', + 'wiki/concepts': 'concept-page', + 'wiki/guides': 'guide-page', + }; + for (const folder of CODEBASE_WIKI_PACK.folders) { + expect(folder.starterTemplate).toBe(expected[folder.path]); + expect(CODEBASE_WIKI_PACK.templates[folder.starterTemplate]).toBeDefined(); + } + }); + + test('ships OVERVIEW + log root files, both prefixed under wiki/', () => { + expect(Object.keys(CODEBASE_WIKI_PACK.rootFiles ?? {}).sort()).toEqual([ + 'wiki/OVERVIEW.md', + 'wiki/log.md', + ]); + }); + + test('OVERVIEW stub carries the profile + source_commit freshness anchors', () => { + const overview = CODEBASE_WIKI_PACK.rootFiles?.['wiki/OVERVIEW.md'] ?? ''; + expect(overview).toContain('profile:'); + expect(overview).toContain('source_commit:'); + }); +}); + describe('buildStarterFolderFrontmatterYaml()', () => { test('emits title + description + tags for a folder', () => { const folder = STARTER_FOLDERS[0]; diff --git a/packages/server/src/seed/starter.ts b/packages/server/src/seed/starter.ts index 8cc5d0a2f..a0effac61 100644 --- a/packages/server/src/seed/starter.ts +++ b/packages/server/src/seed/starter.ts @@ -1,6 +1,7 @@ export type PackId = | 'knowledge-base' | 'software-lifecycle' + | 'codebase-wiki' | 'plain-notes' | 'worldbuilding' | 'writing-pipeline' @@ -374,6 +375,183 @@ tags: [postmortem] `, }; +const CODEBASE_WIKI_FOLDERS: readonly StarterFolder[] = [ + { + path: 'wiki/architecture', + title: 'Architecture', + description: + 'System boundaries, layers, subsystems, and cross-cutting concerns — the big-picture structure. One page per architectural area, each with a `mermaid` system-context or component diagram, the key components, and the design decisions behind them. Uses the `architecture-page` template. Cross-link the modules and flows each area touches. Reference source files per the wiki source-reference convention (relative links + symbol code-spans for `internal`; GitHub blob URLs for `public`).', + tags: ['wiki', 'architecture'], + starterTemplate: 'architecture-page', + }, + { + path: 'wiki/modules', + title: 'Modules', + description: + 'One page per package or module: purpose, responsibilities, public API / entry points, key files (linked per the source-reference convention), dependencies, and the flows it participates in. Uses the `module-page` template. At `depth: tour` these fold into the architecture pages; at `standard`+ each package gets its own page. Cross-link concepts and flows.', + tags: ['wiki', 'module'], + starterTemplate: 'module-page', + }, + { + path: 'wiki/flows', + title: 'Flows', + description: + 'Key end-to-end flows as `mermaid` sequence or flow diagrams plus narrative — how a request, job, or interaction moves through the system. Uses the `flow-page` template; record failure modes at `depth: exhaustive`. Link every module and concept the flow crosses.', + tags: ['wiki', 'flow'], + starterTemplate: 'flow-page', + }, + { + path: 'wiki/concepts', + title: 'Concepts', + description: + 'Glossary of atomic pages for domain terms and core abstractions — the vocabulary a newcomer needs. Uses the `concept-page` template: definition, why it matters, where it lives in the code. Keep pages small and densely cross-linked so concepts become hubs for everywhere they appear.', + tags: ['wiki', 'concept'], + starterTemplate: 'concept-page', + }, + { + path: 'wiki/guides', + title: 'Guides', + description: + 'Task-oriented "how / where do I change X" walkthroughs. Uses the `guide-page` template: goal, steps, relevant code, gotchas. Populated at `depth: standard` and rich at `exhaustive`; thin or empty at `tour`. Link the modules and flows each guide touches.', + tags: ['wiki', 'guide', 'how-to'], + starterTemplate: 'guide-page', + }, +] as const; + +const CODEBASE_WIKI_TEMPLATES: Readonly> = { + 'architecture-page': `--- +template: + title: Architecture Page + description: One subsystem, layer, or cross-cutting concern — boundaries, a diagram, and the key components. +type: architecture +tags: [wiki, architecture] +--- + +## Summary + +## Diagram + +## Key components + +## Design decisions + +## Related +`, + 'module-page': `--- +template: + title: Module Page + description: One package or module — its purpose, public surface, key files, and dependencies. +type: module +tags: [wiki, module] +--- + +## Summary + +## Responsibilities + +## Public API / entry points + +## Key files + +## Dependencies + +## Participates in + +## Related +`, + 'flow-page': `--- +template: + title: Flow Page + description: One end-to-end flow as a sequence diagram, with narrative and failure modes. +type: flow +tags: [wiki, flow] +--- + +## Summary + +## Trigger + +## Sequence diagram + +## Steps + +## Failure modes + +## Related +`, + 'concept-page': `--- +template: + title: Concept Page + description: One domain term or core abstraction — what it means, why it matters, and where it lives in the code. +type: concept +tags: [wiki, concept] +--- + +## Definition + +## Why it matters + +## Where it lives + +## Related +`, + 'guide-page': `--- +template: + title: Guide Page + description: A task-oriented how / where-do-I-change-X walkthrough — goal, steps, relevant code, and gotchas. +type: guide +tags: [wiki, guide] +--- + +## Goal + +## Steps + +## Relevant code + +## Gotchas + +## Related +`, +}; + +const CODEBASE_WIKI_OVERVIEW_MD = `--- +title: Codebase Wiki — Overview +description: Home page and navigation hub for this codebase wiki. Generated and refreshed by the wiki workflow. +profile: +source_commit: +tags: [wiki, overview] +--- + +# Overview + +The home page and navigation hub for this codebase's wiki. It is a stub until you run the wiki workflow — ask your agent to "build the wiki" (optionally naming an audience and depth, e.g. "public, exhaustive"), or call \`workflow({ kind: "wiki" })\` directly. + +Once generated, this page carries: what the project is, a big-picture architecture diagram, and a navigation map linking every section below. + +## What this is + +## Architecture at a glance + +## Navigation + +- Architecture — system boundaries, layers, subsystems +- Modules — one page per package / module +- Flows — key end-to-end sequences +- Concepts — glossary of domain terms and core abstractions +- Guides — task-oriented how / where-do-I-change-X walkthroughs +`; + +const CODEBASE_WIKI_LOG_MD = `--- +title: Wiki Log +description: Append-only audit trail of wiki generation and refresh runs. +--- + +# Wiki Log + +Append-only audit trail. Add one dated entry per generation or refresh run, recording the profile, the \`source_commit\` it was anchored to, and the coverage. The codebase-wiki skill describes the entry shape. +`; + const PLAIN_NOTES_FOLDERS: readonly StarterFolder[] = [ { path: 'notes', @@ -1209,6 +1387,19 @@ export const STARTER_PACKS: Readonly> = { folders: SOFTWARE_LIFECYCLE_FOLDERS, templates: SOFTWARE_LIFECYCLE_TEMPLATES, }, + 'codebase-wiki': { + id: 'codebase-wiki', + name: 'Codebase wiki', + description: + 'An agent-authored, navigable wiki of your codebase — architecture, modules, flows, concepts, and guides, with diagrams, cross-links, and source references. Generated and refreshed by the `wiki` workflow; version-controlled and private by default.', + defaultSubfolder: undefined, + folders: CODEBASE_WIKI_FOLDERS, + templates: CODEBASE_WIKI_TEMPLATES, + rootFiles: { + 'wiki/OVERVIEW.md': CODEBASE_WIKI_OVERVIEW_MD, + 'wiki/log.md': CODEBASE_WIKI_LOG_MD, + }, + }, 'plain-notes': { id: 'plain-notes', name: 'Plain notes',