diff --git a/README.md b/README.md
index 1ce5aa8960..2d97f103af 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,6 @@ pnpm run demo:echo # runnable echo-agent example (no API key needed)
pnpm run demo:coding # the real DeepSeek coding agent (needs DEEPSEEK_API_KEY)
```
-For humans, start with the [development guide](docs/development.md) for local setup, hooks, environment variables, and quality gates, then read the [architecture design](docs/architecture.md) before package work. Local context lives in [packages/](packages/) and [vendor/](vendor/).
+For humans, start with the [development guide](docs/development.md) for local setup, hooks, environment variables, and quality gates, then read the [architecture design](docs/architecture.md) and [documentation graph atlas](docs/graphs/README.md) before package work. Local context lives in [packages/](packages/) and [vendor/](vendor/).
For agents, follow [AGENTS.md](AGENTS.md).
diff --git a/docs/architecture.md b/docs/architecture.md
index 5cd56c6e91..903e58909c 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -8,9 +8,9 @@ The harness core is deliberately tiny: a handful of abstract services plus one c
Requirement context: [Coding Harness MVP 需求分析][mvp-doc].
-For a catalog of the **data structures** this architecture moves around — the core vocabulary types, their literal shapes, and the seam types grouped by capability — see [core-data-structures/](core-data-structures/core.md). This document covers behavior; that one covers the types.
+For a catalog of the **data structures** this architecture moves around — the core vocabulary types, their literal shapes, and the seam types grouped by capability — see [core-data-structures/](core-data-structures/core.md). For visual relationship maps across packages, seams, events, tools, lifecycle, and replay, see the [documentation graph atlas](graphs/README.md). This document covers behavior; those references cover types and topology.
-**Contents:** [Layering](#layering) · [Service map](#service-map) · [Capability seams](#capability-seams-interface--implementation--consumer) · [The vocabulary (dsh-llm)](#the-vocabulary-dsh-llm) · [Event-sourced sessions](#event-sourced-sessions-dsh-session) · [Prompt assembly](#prompt-assembly-dsh-system-prompt) · [Tool pipeline](#tool-pipeline-dsh-tools) · [Agents and the loop](#agents-dsh-agent-and-the-loop-dsh-agent-loop) ([lifecycle](#loop-lifecycle-session--turn--step), [event taxonomy](#event-taxonomy), [waterfall semantics](#cordis-waterfall-semantics-important)) · [Plugin sanity checklist](#plugin-sanity-checklist) · [Extension cookbook](#extension-cookbook) · [Deferred work](#deferred-work-todo)
+**Contents:** [Layering](#layering) · [Service map](#service-map) · [Capability seams](#capability-seams-interface--implementation--consumer) · [The vocabulary (dsh-llm)](#the-vocabulary-dsh-llm) · [Event-sourced sessions](#event-sourced-sessions-dsh-session) · [Prompt assembly](#prompt-assembly-dsh-system-prompt) · [Tool pipeline](#tool-pipeline-dsh-tools) · [Agents and the loop](#agents-dsh-agent-and-the-loop-dsh-agent-loop) ([lifecycle](#loop-lifecycle-session--turn--step), [event taxonomy](#event-taxonomy), [waterfall semantics](#cordis-waterfall-semantics-important)) · [Graph atlas](graphs/README.md) · [Plugin sanity checklist](#plugin-sanity-checklist) · [Extension cookbook](#extension-cookbook) · [Deferred work](#deferred-work-todo)
[microkernel-doc]: https://trtgsjkv6r.feishu.cn/wiki/VS9Lw1kQki6mDJk2UHocyuphnsc
[mvp-doc]: https://trtgsjkv6r.feishu.cn/wiki/ZwK6wfBE9i91V6kzMGYcgRGanxg
diff --git a/docs/development.md b/docs/development.md
index 431d7b4dac..965f23d90f 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -96,9 +96,11 @@ pnpm run lint:fix # eslint . --fix
pnpm run doc-typecheck # compile checked TypeScript snippets in Markdown docs
pnpm run gen-cordis-catalog # regenerate docs/cordis-catalog/events-and-services.md from source
pnpm run verify-cordis-catalog # fail if the cordis events/services catalog is stale
+pnpm run gen-doc-graphs # regenerate docs/graphs/*.md from source and curated graph definitions
+pnpm run verify-doc-graphs # fail if docs/graphs/*.md is stale
pnpm run verify-md-wrap # fail on hard-wrapped prose paragraphs in docs/README markdown
pnpm run verify-type-equiv # fail if a ```ts type-equiv doc block drifts from its source type
-pnpm run doc-sync # doc-typecheck, cordis-catalog freshness, markdown wrap/link, and type-equiv verification
+pnpm run doc-sync # doc-typecheck, generated doc freshness, markdown wrap/link, and type-equiv verification
pnpm run gen-module-graph # regenerate docs/module-graph.md from package peerDeps
pnpm run verify-module-graph # fail if docs/module-graph.md is stale
pnpm run build # emit lib/types intermediates, then bundle lib/index.* runtime files
diff --git a/docs/graphs/README.md b/docs/graphs/README.md
new file mode 100644
index 0000000000..995481d160
--- /dev/null
+++ b/docs/graphs/README.md
@@ -0,0 +1,26 @@
+
+
+# Documentation Graph Atlas
+
+Maintenance mode: mixed: each linked page declares generated, hybrid, or curated mode.
+
+The graph atlas is the relationship layer above the generated catalogs. Use it to navigate package topology, capability seams, event flow, model-facing tools, and runtime lifecycle paths. Exact signatures and type shapes still live in [cordis-catalog/](../cordis-catalog/events-and-services.md), [tool-catalog/](../tool-catalog/tools.md), and [core-data-structures/](../core-data-structures/core.md).
+
+The process decision behind this atlas is recorded in [the documentation graph atlas RFC](../rfc/implemented/process/2026-07-03-documentation-graph-atlas.md).
+
+| Graph | Mode |
+| --- | --- |
+| [package topology by group](package-topology.md) | `generated` |
+| [capability seams and core services](capability-seams.md) | `hybrid generated` |
+| [app composition](app-composition.md) | `hybrid generated` |
+| [event producer/consumer matrix](event-producer-consumer.md) | `hybrid generated` |
+| [tool affordance map](tool-affordance-map.md) | `hybrid generated` |
+| [agent turn and step lifecycle](agent-lifecycle.md) | `curated` |
+| [tool execution pipeline](tool-execution-pipeline.md) | `curated` |
+| [session surface and message projection](session-surface.md) | `curated` |
+| [subagent and session lineage](subagent-lineage.md) | `curated` |
+| [plugin disposal and hot reload ownership](hot-reload-disposal.md) | `curated` |
+| [ACP snapshot replay](snapshot-replay.md) | `curated` |
+
+Regenerate with `pnpm run gen-doc-graphs`; verify freshness with `pnpm run verify-doc-graphs`.
diff --git a/docs/graphs/agent-lifecycle.md b/docs/graphs/agent-lifecycle.md
new file mode 100644
index 0000000000..cc25e25694
--- /dev/null
+++ b/docs/graphs/agent-lifecycle.md
@@ -0,0 +1,41 @@
+
+
+# Agent Turn And Step Lifecycle
+
+Maintenance mode: curated Mermaid sequence; exact event signatures live in the generated Cordis catalog.
+
+This sequence is the visual companion to [architecture.md](../architecture.md#loop-lifecycle-session--turn--step). It shows the durable session event path separately from live `agent/*` notifications.
+
+```mermaid
+sequenceDiagram
+ participant User
+ participant Agent
+ participant Loop
+ participant Prompt as ctx.systemPrompt
+ participant LLM as ctx.llm
+ participant Tools as ctx.tools
+ participant Session
+ participant Persistence
+ User->>Agent: send(content)
+ Agent->>Loop: queued work wakes driver
+ Loop->>Session: turn/start + user/message
+ Loop-->>User: agent/turn-start
+ Loop->>Prompt: system-prompt/assemble waterfall
+ Loop-->>Loop: agent/pre-step serial checkpoint
+ Loop->>Session: step/start
+ Loop->>LLM: agent/request waterfall, then llm/stream waterfall
+ LLM-->>Loop: StreamChunk*
+ Loop->>Session: assistant/chunk*
+ Loop-->>User: agent/stream-chunk* (master live mirror)
+ Loop->>Session: assistant/message
+ Loop->>Tools: tools/execute waterfall for each tool-call
+ Tools-->>Session: tool-owned events when applicable
+ Loop->>Session: tool/result
+ Loop-->>Loop: agent/turn-continuation waterfall
+ Loop->>Session: turn/end
+ Loop->>Persistence: session/flush parallel checkpoint
+ Loop-->>User: agent/status idle
+```
+
+Future pressure from the hooks stack: PR #129 removes the live `agent/stream-chunk` mirror and leaves durable `assistant/chunk` on `session/event` as the authoritative token stream. Consumers that need replayable transcript data should already treat `session/event` as the load-bearing path.
diff --git a/docs/graphs/app-composition.md b/docs/graphs/app-composition.md
new file mode 100644
index 0000000000..ce2bab32d8
--- /dev/null
+++ b/docs/graphs/app-composition.md
@@ -0,0 +1,90 @@
+
+
+# App Composition
+
+Maintenance mode: hybrid: leaf plugin lists are parsed from `examples/*/cordis.yml`; bundle expansions are curated from app package source.
+
+This graph is for SDK users asking which pieces a runnable agent loads. Leaf configs choose adapters and optional product tools; app packages provide the front door; `dsh-agent-core` bundles the providerless spine.
+
+```mermaid
+flowchart LR
+ subgraph example_echo["examples/echo-agent"]
+ cfg_echo["cordis.yml"]
+ plugin_echo_hmr["hmr
@cordisjs/plugin-hmr"]
+ cfg_echo --> plugin_echo_hmr
+ plugin_echo_mock_llm["mock-llm
./src/mock-llm.ts"]
+ cfg_echo --> plugin_echo_mock_llm
+ plugin_echo_echo_tool["echo-tool
./src/echo-tool.ts"]
+ cfg_echo --> plugin_echo_echo_tool
+ plugin_echo_bash["bash
@deepseek-ai/dsh-bash-local"]
+ cfg_echo --> plugin_echo_bash
+ plugin_echo_stdio_agent["stdio-agent
@deepseek-ai/dsh-stdio-agent"]
+ cfg_echo --> plugin_echo_stdio_agent
+ plugin_echo_stdio_agent --> bundle_stdio
+ end
+ subgraph example_coding["examples/coding-agent"]
+ cfg_coding["cordis.yml"]
+ plugin_coding_hmr["hmr
@cordisjs/plugin-hmr"]
+ cfg_coding --> plugin_coding_hmr
+ plugin_coding_llm_deepseek["llm-deepseek
@deepseek-ai/dsh-llm-deepseek"]
+ cfg_coding --> plugin_coding_llm_deepseek
+ plugin_coding_bash["bash
@deepseek-ai/dsh-bash-local"]
+ cfg_coding --> plugin_coding_bash
+ plugin_coding_stdio_agent["stdio-agent
@deepseek-ai/dsh-stdio-agent"]
+ cfg_coding --> plugin_coding_stdio_agent
+ plugin_coding_stdio_agent --> bundle_stdio
+ plugin_coding_compact_basic["compact-basic
@deepseek-ai/dsh-compact-basic"]
+ cfg_coding --> plugin_coding_compact_basic
+ plugin_coding_subagent["subagent
@deepseek-ai/dsh-subagent"]
+ cfg_coding --> plugin_coding_subagent
+ plugin_coding_subagent_spawn["subagent-spawn
@deepseek-ai/dsh-subagent-spawn"]
+ cfg_coding --> plugin_coding_subagent_spawn
+ plugin_coding_subagent_fork["subagent-fork
@deepseek-ai/dsh-subagent-fork"]
+ cfg_coding --> plugin_coding_subagent_fork
+ plugin_coding_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"]
+ cfg_coding --> plugin_coding_tool_subagent
+ plugin_coding_tool_subagent_fork["tool-subagent-fork
@deepseek-ai/dsh-tool-subagent"]
+ cfg_coding --> plugin_coding_tool_subagent_fork
+ plugin_coding_tool_todo["tool-todo
@deepseek-ai/dsh-tool-todo"]
+ cfg_coding --> plugin_coding_tool_todo
+ end
+ subgraph example_acp["examples/acp-agent"]
+ cfg_acp["cordis.yml"]
+ plugin_acp_llm_deepseek["llm-deepseek
@deepseek-ai/dsh-llm-deepseek"]
+ cfg_acp --> plugin_acp_llm_deepseek
+ plugin_acp_bash["bash
@deepseek-ai/dsh-bash-local"]
+ cfg_acp --> plugin_acp_bash
+ plugin_acp_acp_agent["acp-agent
@deepseek-ai/dsh-acp-agent"]
+ cfg_acp --> plugin_acp_acp_agent
+ plugin_acp_acp_agent --> bundle_acp_agent
+ plugin_acp_subagent["subagent
@deepseek-ai/dsh-subagent"]
+ cfg_acp --> plugin_acp_subagent
+ plugin_acp_subagent_spawn["subagent-spawn
@deepseek-ai/dsh-subagent-spawn"]
+ cfg_acp --> plugin_acp_subagent_spawn
+ plugin_acp_subagent_fork["subagent-fork
@deepseek-ai/dsh-subagent-fork"]
+ cfg_acp --> plugin_acp_subagent_fork
+ plugin_acp_tool_subagent["tool-subagent
@deepseek-ai/dsh-tool-subagent"]
+ cfg_acp --> plugin_acp_tool_subagent
+ plugin_acp_tool_subagent_fork["tool-subagent-fork
@deepseek-ai/dsh-tool-subagent"]
+ cfg_acp --> plugin_acp_tool_subagent_fork
+ plugin_acp_tool_todo["tool-todo
@deepseek-ai/dsh-tool-todo"]
+ cfg_acp --> plugin_acp_tool_todo
+ end
+ bundle_stdio["@deepseek-ai/dsh-stdio-agent"] --> bundle_agent_core["@deepseek-ai/dsh-agent-core"]
+ bundle_stdio --> bundle_jsonl["@deepseek-ai/dsh-session-persistence-jsonl"]
+ bundle_stdio --> bundle_ui_stdio["@deepseek-ai/dsh-ui-stdio"]
+ bundle_acp_agent["@deepseek-ai/dsh-acp-agent"] --> bundle_agent_core
+ bundle_acp_agent --> bundle_jsonl
+ bundle_acp_agent --> bundle_acp["@deepseek-ai/dsh-acp"]
+ bundle_agent_core --> spine_llm["ctx.llm"]
+ bundle_agent_core --> spine_sessions["ctx.sessions"]
+ bundle_agent_core --> spine_tools["ctx.tools + tool-bash"]
+ bundle_agent_core --> spine_loop["ctx.agents + ctx.agentLoop"]
+```
+
+| Example | Parsed plugin ids | Config |
+| --- | --- | --- |
+| `examples/echo-agent` | `hmr`, `mock-llm`, `echo-tool`, `bash`, `stdio-agent` | [`examples/echo-agent/cordis.yml`](../../examples/echo-agent/cordis.yml) |
+| `examples/coding-agent` | `hmr`, `llm-deepseek`, `bash`, `stdio-agent`, `compact-basic`, `subagent`, `subagent-spawn`, `subagent-fork`, `tool-subagent`, `tool-subagent-fork`, `tool-todo` | [`examples/coding-agent/cordis.yml`](../../examples/coding-agent/cordis.yml) |
+| `examples/acp-agent` | `llm-deepseek`, `bash`, `acp-agent`, `subagent`, `subagent-spawn`, `subagent-fork`, `tool-subagent`, `tool-subagent-fork`, `tool-todo` | [`examples/acp-agent/cordis.yml`](../../examples/acp-agent/cordis.yml) |
diff --git a/docs/graphs/capability-seams.md b/docs/graphs/capability-seams.md
new file mode 100644
index 0000000000..d3b95b55f8
--- /dev/null
+++ b/docs/graphs/capability-seams.md
@@ -0,0 +1,110 @@
+
+
+# Capability Seams And Core Services
+
+Maintenance mode: hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard.
+
+A service can be a core spine service, a swappable capability seam, or a bundle/composition point. The graph shows the package that owns the service declaration, known implementation packages, and packages that consume the service directly.
+
+```mermaid
+flowchart LR
+ pkg_llm["llm"]
+ svc_llm["ctx.llm
LLM adapter registry"]
+ pkg_llm_deepseek["llm-deepseek"]
+ pkg_llm_pi_ai["llm-pi-ai"]
+ pkg_llm_replay["llm-replay"]
+ pkg_agent_loop["agent-loop"]
+ pkg_compact_basic["compact-basic"]
+ pkg_session["session"]
+ svc_sessions["ctx.sessions
In-memory session store"]
+ pkg_agent["agent"]
+ pkg_session_persistence["session-persistence"]
+ pkg_subagent_inprocess["subagent-inprocess"]
+ pkg_invariants["invariants"]
+ svc_sessionPersistence["ctx.sessionPersistence
Durable session persistence seam"]
+ pkg_session_persistence_jsonl["session-persistence-jsonl"]
+ pkg_session_persistence_sqlite["session-persistence-sqlite"]
+ pkg_acp["acp"]
+ pkg_system_prompt["system-prompt"]
+ svc_systemPrompt["ctx.systemPrompt
System prompt assembly registry"]
+ pkg_tools["tools"]
+ svc_tools["ctx.tools
Tool registry and execution waterfall"]
+ pkg_tool_bash["tool-bash"]
+ pkg_tool_subagent["tool-subagent"]
+ pkg_tool_todo["tool-todo"]
+ svc_agents["ctx.agents
Agent registry"]
+ pkg_stdio_agent["stdio-agent"]
+ svc_agentLoop["ctx.agentLoop
Concrete loop driver"]
+ pkg_agent_core["agent-core"]
+ pkg_bash["bash"]
+ svc_bash["ctx.bash
Bash executor seam"]
+ pkg_bash_local["bash-local"]
+ pkg_compact["compact"]
+ svc_compact["ctx.compact
Compaction seam"]
+ pkg_subagent["subagent"]
+ svc_subagents["ctx.subagents
Subagent provider registry"]
+ pkg_subagent_spawn["subagent-spawn"]
+ pkg_subagent_fork["subagent-fork"]
+ pkg_subagent_acp["subagent-acp"]
+ pkg_subagent_mock["subagent-mock"]
+ pkg_agent --> svc_agents
+ pkg_agent_loop --> svc_agentLoop
+ pkg_bash --> svc_bash
+ pkg_bash_local --> svc_bash
+ pkg_compact --> svc_compact
+ pkg_compact_basic --> svc_compact
+ pkg_llm --> svc_llm
+ pkg_llm_deepseek --> svc_llm
+ pkg_llm_pi_ai --> svc_llm
+ pkg_llm_replay --> svc_llm
+ pkg_session --> svc_sessions
+ pkg_session_persistence --> svc_sessionPersistence
+ pkg_session_persistence_jsonl --> svc_sessionPersistence
+ pkg_session_persistence_sqlite --> svc_sessionPersistence
+ pkg_subagent --> svc_subagents
+ pkg_subagent_acp --> svc_subagents
+ pkg_subagent_fork --> svc_subagents
+ pkg_subagent_mock --> svc_subagents
+ pkg_subagent_spawn --> svc_subagents
+ pkg_system_prompt --> svc_systemPrompt
+ pkg_tools --> svc_tools
+ svc_agentLoop --> pkg_agent_core
+ svc_agents --> pkg_acp
+ svc_agents --> pkg_agent_loop
+ svc_agents --> pkg_invariants
+ svc_agents --> pkg_stdio_agent
+ svc_agents --> pkg_subagent_inprocess
+ svc_bash --> pkg_tool_bash
+ svc_compact --> pkg_compact_basic
+ svc_llm --> pkg_agent_loop
+ svc_llm --> pkg_compact_basic
+ svc_sessionPersistence --> pkg_acp
+ svc_sessionPersistence --> pkg_agent_loop
+ svc_sessions --> pkg_agent
+ svc_sessions --> pkg_agent_loop
+ svc_sessions --> pkg_invariants
+ svc_sessions --> pkg_session_persistence
+ svc_sessions --> pkg_subagent_inprocess
+ svc_subagents --> pkg_tool_subagent
+ svc_systemPrompt --> pkg_agent_loop
+ svc_systemPrompt --> pkg_tools
+ svc_tools --> pkg_acp
+ svc_tools --> pkg_agent_loop
+ svc_tools --> pkg_tool_bash
+ svc_tools --> pkg_tool_subagent
+ svc_tools --> pkg_tool_todo
+```
+
+| ctx key | Role | Owner | Implementations | Direct consumers | Note |
+| --- | --- | --- | --- | --- | --- |
+| `ctx.llm` | `seam` | [`llm`](../../packages/llm/llm) | [`llm-deepseek`](../../packages/llm/llm-deepseek), [`llm-pi-ai`](../../packages/llm/llm-pi-ai), [`llm-replay`](../../packages/support/llm-replay) | [`agent-loop`](../../packages/core/agent-loop), [`compact-basic`](../../packages/compact/compact-basic) | Adapters register provider implementations; the loop and compaction call the provider-neutral stream service. |
+| `ctx.sessions` | `core` | [`session`](../../packages/core/session) | - | [`agent-loop`](../../packages/core/agent-loop), [`agent`](../../packages/core/agent), [`session-persistence`](../../packages/session-persistence/session-persistence), [`subagent-inprocess`](../../packages/subagent/subagent-inprocess), [`invariants`](../../packages/support/invariants) | Owns append-only Session instances and emits the durable session event feed. |
+| `ctx.sessionPersistence` | `seam` | [`session-persistence`](../../packages/session-persistence/session-persistence) | [`session-persistence-jsonl`](../../packages/session-persistence/session-persistence-jsonl), [`session-persistence-sqlite`](../../packages/session-persistence/session-persistence-sqlite) | [`agent-loop`](../../packages/core/agent-loop), [`acp`](../../packages/ui/acp) | Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time. |
+| `ctx.systemPrompt` | `core` | [`system-prompt`](../../packages/core/system-prompt) | - | [`agent-loop`](../../packages/core/agent-loop), [`tools`](../../packages/core/tools) | Collects prompt sections and model-facing tool schemas for each step. |
+| `ctx.tools` | `core` | [`tools`](../../packages/core/tools) | - | [`agent-loop`](../../packages/core/agent-loop), [`tool-bash`](../../packages/bash/tool-bash), [`tool-subagent`](../../packages/subagent/tool-subagent), [`tool-todo`](../../packages/todo/tool-todo), [`acp`](../../packages/ui/acp) | Registers tool definitions, exposes schemas to the prompt, and routes calls through tools/execute. |
+| `ctx.agents` | `core` | [`agent`](../../packages/core/agent) | - | [`agent-loop`](../../packages/core/agent-loop), [`acp`](../../packages/ui/acp), [`subagent-inprocess`](../../packages/subagent/subagent-inprocess), [`stdio-agent`](../../packages/ui/stdio-agent), [`invariants`](../../packages/support/invariants) | Owns live Agent handles and the create/resume factory seam. |
+| `ctx.agentLoop` | `bundle` | [`agent-loop`](../../packages/core/agent-loop) | - | [`agent-core`](../../packages/core/agent-core) | The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package. |
+| `ctx.bash` | `seam` | [`bash`](../../packages/bash/bash) | [`bash-local`](../../packages/bash/bash-local) | [`tool-bash`](../../packages/bash/tool-bash) | The model-facing bash tools consume this seam; sandboxed or remote executors can replace bash-local. |
+| `ctx.compact` | `seam` | [`compact`](../../packages/compact/compact) | [`compact-basic`](../../packages/compact/compact-basic) | [`compact-basic`](../../packages/compact/compact-basic) | The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred. |
+| `ctx.subagents` | `seam` | [`subagent`](../../packages/subagent/subagent) | [`subagent-spawn`](../../packages/subagent/subagent-spawn), [`subagent-fork`](../../packages/subagent/subagent-fork), [`subagent-acp`](../../packages/subagent/subagent-acp), [`subagent-mock`](../../packages/support/subagent-mock) | [`tool-subagent`](../../packages/subagent/tool-subagent) | Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name. |
diff --git a/docs/graphs/event-producer-consumer.md b/docs/graphs/event-producer-consumer.md
new file mode 100644
index 0000000000..dfce3fe04a
--- /dev/null
+++ b/docs/graphs/event-producer-consumer.md
@@ -0,0 +1,36 @@
+
+
+# Event Producer And Consumer Matrix
+
+Maintenance mode: hybrid generated: Cordis event declarations and most producer/listener edges are AST-scanned; dynamic dispatch sites are classified in `scripts/gen-doc-graphs.ts`.
+
+This matrix shows which packages dispatch each harness-owned event and which packages listen to it. It is intentionally a table rather than one large graph: events are many-to-many, and dense relation data is easier to review in rows. Dynamic dispatch overrides cover sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.
+
+| Event | Mode | Declared in | Dispatchers | Listeners |
+| --- | --- | --- | --- | --- |
+| `agent/created` | `emit` | [`packages/core/agent/src/types.ts:137`](../../packages/core/agent/src/types.ts) | [`agent`](../../packages/core/agent) (`emit`) | - |
+| `agent/disposed` | `emit` | [`packages/core/agent/src/types.ts:143`](../../packages/core/agent/src/types.ts) | [`agent`](../../packages/core/agent) (`emit`) | - |
+| `agent/error` | `emit` | [`packages/core/agent/src/types.ts:254`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
+| `agent/pre-step` | `serial` | [`packages/core/agent/src/types.ts:214`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`serial`) | [`compact-basic`](../../packages/compact/compact-basic) |
+| `agent/queued` | `emit` | [`packages/core/agent/src/types.ts:156`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
+| `agent/request` | `waterfall` | [`packages/core/agent/src/types.ts:223`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`waterfall`), [`compact-basic`](../../packages/compact/compact-basic) (`waterfall`) | - |
+| `agent/status` | `emit` | [`packages/core/agent/src/types.ts:150`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | [`acp`](../../packages/ui/acp), [`invariants`](../../packages/support/invariants), [`ui-stdio`](../../packages/support/ui-stdio) |
+| `agent/steering` | `emit` | [`packages/core/agent/src/types.ts:248`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
+| `agent/step-end` | `emit` | [`packages/core/agent/src/types.ts:180`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
+| `agent/step-result` | `waterfall` | [`packages/core/agent/src/types.ts:229`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`waterfall`) | - |
+| `agent/step-start` | `emit` | [`packages/core/agent/src/types.ts:175`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | - |
+| `agent/stream-chunk` | `emit` | [`packages/core/agent/src/types.ts:243`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | [`ui-stdio`](../../packages/support/ui-stdio) |
+| `agent/turn-continuation` | `waterfall` | [`packages/core/agent/src/types.ts:236`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`waterfall`) | - |
+| `agent/turn-end` | `emit` | [`packages/core/agent/src/types.ts:169`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | [`ui-stdio`](../../packages/support/ui-stdio) |
+| `agent/turn-start` | `emit` | [`packages/core/agent/src/types.ts:163`](../../packages/core/agent/src/types.ts) | [`agent-loop`](../../packages/core/agent-loop) (`emit`) | [`ui-stdio`](../../packages/support/ui-stdio) |
+| `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:31`](../../packages/llm/llm/src/index.ts) | [`llm`](../../packages/llm/llm) (`waterfall`) | [`llm-replay`](../../packages/support/llm-replay) |
+| `session/created` | `emit` | [`packages/core/session/src/index.ts:34`](../../packages/core/session/src/index.ts) | [`session`](../../packages/core/session) (`emit`) | [`invariants`](../../packages/support/invariants), [`session-persistence`](../../packages/session-persistence/session-persistence) |
+| `session/event` | `emit` | [`packages/core/session/src/index.ts:40`](../../packages/core/session/src/index.ts) | [`session`](../../packages/core/session) (`emit`) | [`acp`](../../packages/ui/acp), [`invariants`](../../packages/support/invariants), [`session-persistence`](../../packages/session-persistence/session-persistence), [`ui-stdio`](../../packages/support/ui-stdio) |
+| `session/flush` | `parallel` | [`packages/core/session/src/index.ts:49`](../../packages/core/session/src/index.ts) | [`agent-loop`](../../packages/core/agent-loop) (`parallel`) | [`session-persistence`](../../packages/session-persistence/session-persistence) |
+| `subagent/end` | `emit` | [`packages/subagent/subagent/src/index.ts:65`](../../packages/subagent/subagent/src/index.ts) | [`subagent`](../../packages/subagent/subagent) (`events.dispatch`) | - |
+| `subagent/start` | `emit` | [`packages/subagent/subagent/src/index.ts:59`](../../packages/subagent/subagent/src/index.ts) | [`subagent`](../../packages/subagent/subagent) (`events.dispatch`) | - |
+| `system-prompt/assemble` | `waterfall` | [`packages/core/system-prompt/src/index.ts:24`](../../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../../packages/core/system-prompt) (`waterfall`) | - |
+| `system-prompt/change` | `emit` | [`packages/core/system-prompt/src/index.ts:30`](../../packages/core/system-prompt/src/index.ts) | [`system-prompt`](../../packages/core/system-prompt) (`emit`) | - |
+| `tools/change` | `emit` | [`packages/core/tools/src/index.ts:48`](../../packages/core/tools/src/index.ts) | [`tools`](../../packages/core/tools) (`emit`) | - |
+| `tools/execute` | `waterfall` | [`packages/core/tools/src/index.ts:43`](../../packages/core/tools/src/index.ts) | [`tools`](../../packages/core/tools) (`waterfall`) | - |
diff --git a/docs/graphs/hot-reload-disposal.md b/docs/graphs/hot-reload-disposal.md
new file mode 100644
index 0000000000..ca98c25692
--- /dev/null
+++ b/docs/graphs/hot-reload-disposal.md
@@ -0,0 +1,24 @@
+
+
+# Plugin Disposal And Hot Reload Ownership
+
+Maintenance mode: curated Mermaid flow based on Cordis fiber/effect conventions.
+
+This graph is a maintainer checklist for plugin authors: registrations are effects, service injection gates activation, and owned handles must be disposed by their owner.
+
+```mermaid
+flowchart TD
+ plugin["ctx.plugin(plugin) creates fiber"]
+ inject["static inject gates activation"]
+ service["ctx.provide / Service constructor"]
+ effects["ctx.effect registrations
events, tools, adapters, timers"]
+ reload["HMR / fiber.dispose()"]
+ disposers["Run disposers in owner fiber"]
+ quiescence["Owned AgentHandle.dispose()
or service teardown awaits quiescence"]
+ plugin --> inject --> service
+ inject --> effects
+ reload --> disposers --> quiescence
+```
+
+Hook bridges and SDK plugins increase the number of long-lived listeners, so this ownership graph should stay small and visible.
diff --git a/docs/graphs/package-topology.md b/docs/graphs/package-topology.md
new file mode 100644
index 0000000000..68d5a2442c
--- /dev/null
+++ b/docs/graphs/package-topology.md
@@ -0,0 +1,189 @@
+
+
+# Package Topology By Group
+
+Maintenance mode: generated from `packages/*/*/package.json` peer dependencies plus package group paths.
+
+This graph complements [module-graph.md](../module-graph.md): it keeps the same canonical peer-dependency edge source, but clusters packages by the `packages//` hierarchy so layering and capability families are easier to scan.
+
+```mermaid
+flowchart TD
+ subgraph group_util["packages/util"]
+ pkg_brand["brand"]
+ end
+ subgraph group_llm["packages/llm"]
+ pkg_llm["llm"]
+ pkg_llm_deepseek["llm-deepseek"]
+ pkg_llm_pi_ai["llm-pi-ai"]
+ end
+ subgraph group_core["packages/core"]
+ pkg_agent["agent"]
+ pkg_agent_core["agent-core"]
+ pkg_agent_loop["agent-loop"]
+ pkg_session["session"]
+ pkg_system_prompt["system-prompt"]
+ pkg_tools["tools"]
+ end
+ subgraph group_bash["packages/bash"]
+ pkg_bash["bash"]
+ pkg_bash_local["bash-local"]
+ pkg_tool_bash["tool-bash"]
+ end
+ subgraph group_compact["packages/compact"]
+ pkg_compact["compact"]
+ pkg_compact_basic["compact-basic"]
+ end
+ subgraph group_subagent["packages/subagent"]
+ pkg_subagent["subagent"]
+ pkg_subagent_acp["subagent-acp"]
+ pkg_subagent_fork["subagent-fork"]
+ pkg_subagent_inprocess["subagent-inprocess"]
+ pkg_subagent_spawn["subagent-spawn"]
+ pkg_tool_subagent["tool-subagent"]
+ end
+ subgraph group_session_persistence["packages/session-persistence"]
+ pkg_session_persistence["session-persistence"]
+ pkg_session_persistence_jsonl["session-persistence-jsonl"]
+ pkg_session_persistence_sqlite["session-persistence-sqlite"]
+ end
+ subgraph group_todo["packages/todo"]
+ pkg_tool_todo["tool-todo"]
+ end
+ subgraph group_support["packages/support"]
+ pkg_invariants["invariants"]
+ pkg_llm_replay["llm-replay"]
+ pkg_subagent_mock["subagent-mock"]
+ pkg_ui_stdio["ui-stdio"]
+ end
+ subgraph group_ui["packages/ui"]
+ pkg_acp["acp"]
+ pkg_acp_agent["acp-agent"]
+ pkg_stdio_agent["stdio-agent"]
+ end
+ pkg_llm --> pkg_brand
+ pkg_bash --> pkg_brand
+ pkg_llm_deepseek --> pkg_llm
+ pkg_llm_pi_ai --> pkg_llm
+ pkg_session --> pkg_brand
+ pkg_session --> pkg_llm
+ pkg_system_prompt --> pkg_llm
+ pkg_bash_local --> pkg_bash
+ pkg_agent --> pkg_brand
+ pkg_agent --> pkg_llm
+ pkg_agent --> pkg_session
+ pkg_compact --> pkg_llm
+ pkg_compact --> pkg_session
+ pkg_session_persistence --> pkg_session
+ pkg_llm_replay --> pkg_llm
+ pkg_llm_replay --> pkg_session
+ pkg_tools --> pkg_agent
+ pkg_tools --> pkg_llm
+ pkg_tools --> pkg_system_prompt
+ pkg_compact_basic --> pkg_agent
+ pkg_compact_basic --> pkg_compact
+ pkg_compact_basic --> pkg_llm
+ pkg_compact_basic --> pkg_session
+ pkg_session_persistence_jsonl --> pkg_session
+ pkg_session_persistence_jsonl --> pkg_session_persistence
+ pkg_session_persistence_sqlite --> pkg_session
+ pkg_session_persistence_sqlite --> pkg_session_persistence
+ pkg_invariants --> pkg_agent
+ pkg_invariants --> pkg_llm
+ pkg_invariants --> pkg_session
+ pkg_ui_stdio --> pkg_agent
+ pkg_ui_stdio --> pkg_llm
+ pkg_ui_stdio --> pkg_session
+ pkg_agent_loop --> pkg_agent
+ pkg_agent_loop --> pkg_llm
+ pkg_agent_loop --> pkg_session
+ pkg_agent_loop --> pkg_session_persistence
+ pkg_agent_loop --> pkg_system_prompt
+ pkg_agent_loop --> pkg_tools
+ pkg_tool_bash --> pkg_agent
+ pkg_tool_bash --> pkg_bash
+ pkg_tool_bash --> pkg_llm
+ pkg_tool_bash --> pkg_tools
+ pkg_subagent --> pkg_agent
+ pkg_subagent --> pkg_llm
+ pkg_subagent --> pkg_tools
+ pkg_tool_todo --> pkg_agent
+ pkg_tool_todo --> pkg_session
+ pkg_tool_todo --> pkg_tools
+ pkg_acp --> pkg_agent
+ pkg_acp --> pkg_llm
+ pkg_acp --> pkg_session
+ pkg_acp --> pkg_session_persistence
+ pkg_acp --> pkg_tools
+ pkg_agent_core --> pkg_agent
+ pkg_agent_core --> pkg_agent_loop
+ pkg_agent_core --> pkg_invariants
+ pkg_agent_core --> pkg_llm
+ pkg_agent_core --> pkg_session
+ pkg_agent_core --> pkg_system_prompt
+ pkg_agent_core --> pkg_tool_bash
+ pkg_agent_core --> pkg_tools
+ pkg_subagent_acp --> pkg_agent
+ pkg_subagent_acp --> pkg_llm
+ pkg_subagent_acp --> pkg_subagent
+ pkg_subagent_inprocess --> pkg_agent
+ pkg_subagent_inprocess --> pkg_llm
+ pkg_subagent_inprocess --> pkg_session
+ pkg_subagent_inprocess --> pkg_subagent
+ pkg_tool_subagent --> pkg_agent
+ pkg_tool_subagent --> pkg_llm
+ pkg_tool_subagent --> pkg_subagent
+ pkg_tool_subagent --> pkg_tools
+ pkg_subagent_mock --> pkg_agent
+ pkg_subagent_mock --> pkg_llm
+ pkg_subagent_mock --> pkg_subagent
+ pkg_subagent_fork --> pkg_agent
+ pkg_subagent_fork --> pkg_session
+ pkg_subagent_fork --> pkg_subagent
+ pkg_subagent_fork --> pkg_subagent_inprocess
+ pkg_subagent_spawn --> pkg_subagent
+ pkg_subagent_spawn --> pkg_subagent_inprocess
+ pkg_acp_agent --> pkg_acp
+ pkg_acp_agent --> pkg_agent_core
+ pkg_acp_agent --> pkg_session_persistence_jsonl
+ pkg_stdio_agent --> pkg_agent
+ pkg_stdio_agent --> pkg_agent_core
+ pkg_stdio_agent --> pkg_session
+ pkg_stdio_agent --> pkg_session_persistence_jsonl
+ pkg_stdio_agent --> pkg_ui_stdio
+```
+
+| Package | Group | Depends on |
+| --- | --- | --- |
+| [`brand`](../../packages/util/brand) | `util` | - |
+| [`llm`](../../packages/llm/llm) | `llm` | [`brand`](../../packages/util/brand) |
+| [`bash`](../../packages/bash/bash) | `bash` | [`brand`](../../packages/util/brand) |
+| [`llm-deepseek`](../../packages/llm/llm-deepseek) | `llm` | [`llm`](../../packages/llm/llm) |
+| [`llm-pi-ai`](../../packages/llm/llm-pi-ai) | `llm` | [`llm`](../../packages/llm/llm) |
+| [`session`](../../packages/core/session) | `core` | [`brand`](../../packages/util/brand), [`llm`](../../packages/llm/llm) |
+| [`system-prompt`](../../packages/core/system-prompt) | `core` | [`llm`](../../packages/llm/llm) |
+| [`bash-local`](../../packages/bash/bash-local) | `bash` | [`bash`](../../packages/bash/bash) |
+| [`agent`](../../packages/core/agent) | `core` | [`brand`](../../packages/util/brand), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
+| [`compact`](../../packages/compact/compact) | `compact` | [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
+| [`session-persistence`](../../packages/session-persistence/session-persistence) | `session-persistence` | [`session`](../../packages/core/session) |
+| [`llm-replay`](../../packages/support/llm-replay) | `support` | [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
+| [`tools`](../../packages/core/tools) | `core` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`system-prompt`](../../packages/core/system-prompt) |
+| [`compact-basic`](../../packages/compact/compact-basic) | `compact` | [`agent`](../../packages/core/agent), [`compact`](../../packages/compact/compact), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
+| [`session-persistence-jsonl`](../../packages/session-persistence/session-persistence-jsonl) | `session-persistence` | [`session`](../../packages/core/session), [`session-persistence`](../../packages/session-persistence/session-persistence) |
+| [`session-persistence-sqlite`](../../packages/session-persistence/session-persistence-sqlite) | `session-persistence` | [`session`](../../packages/core/session), [`session-persistence`](../../packages/session-persistence/session-persistence) |
+| [`invariants`](../../packages/support/invariants) | `support` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
+| [`ui-stdio`](../../packages/support/ui-stdio) | `support` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session) |
+| [`agent-loop`](../../packages/core/agent-loop) | `core` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session), [`session-persistence`](../../packages/session-persistence/session-persistence), [`system-prompt`](../../packages/core/system-prompt), [`tools`](../../packages/core/tools) |
+| [`tool-bash`](../../packages/bash/tool-bash) | `bash` | [`agent`](../../packages/core/agent), [`bash`](../../packages/bash/bash), [`llm`](../../packages/llm/llm), [`tools`](../../packages/core/tools) |
+| [`subagent`](../../packages/subagent/subagent) | `subagent` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`tools`](../../packages/core/tools) |
+| [`tool-todo`](../../packages/todo/tool-todo) | `todo` | [`agent`](../../packages/core/agent), [`session`](../../packages/core/session), [`tools`](../../packages/core/tools) |
+| [`acp`](../../packages/ui/acp) | `ui` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session), [`session-persistence`](../../packages/session-persistence/session-persistence), [`tools`](../../packages/core/tools) |
+| [`agent-core`](../../packages/core/agent-core) | `core` | [`agent`](../../packages/core/agent), [`agent-loop`](../../packages/core/agent-loop), [`invariants`](../../packages/support/invariants), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session), [`system-prompt`](../../packages/core/system-prompt), [`tool-bash`](../../packages/bash/tool-bash), [`tools`](../../packages/core/tools) |
+| [`subagent-acp`](../../packages/subagent/subagent-acp) | `subagent` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`subagent`](../../packages/subagent/subagent) |
+| [`subagent-inprocess`](../../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`session`](../../packages/core/session), [`subagent`](../../packages/subagent/subagent) |
+| [`tool-subagent`](../../packages/subagent/tool-subagent) | `subagent` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`subagent`](../../packages/subagent/subagent), [`tools`](../../packages/core/tools) |
+| [`subagent-mock`](../../packages/support/subagent-mock) | `support` | [`agent`](../../packages/core/agent), [`llm`](../../packages/llm/llm), [`subagent`](../../packages/subagent/subagent) |
+| [`subagent-fork`](../../packages/subagent/subagent-fork) | `subagent` | [`agent`](../../packages/core/agent), [`session`](../../packages/core/session), [`subagent`](../../packages/subagent/subagent), [`subagent-inprocess`](../../packages/subagent/subagent-inprocess) |
+| [`subagent-spawn`](../../packages/subagent/subagent-spawn) | `subagent` | [`subagent`](../../packages/subagent/subagent), [`subagent-inprocess`](../../packages/subagent/subagent-inprocess) |
+| [`acp-agent`](../../packages/ui/acp-agent) | `ui` | [`acp`](../../packages/ui/acp), [`agent-core`](../../packages/core/agent-core), [`session-persistence-jsonl`](../../packages/session-persistence/session-persistence-jsonl) |
+| [`stdio-agent`](../../packages/ui/stdio-agent) | `ui` | [`agent`](../../packages/core/agent), [`agent-core`](../../packages/core/agent-core), [`session`](../../packages/core/session), [`session-persistence-jsonl`](../../packages/session-persistence/session-persistence-jsonl), [`ui-stdio`](../../packages/support/ui-stdio) |
diff --git a/docs/graphs/session-surface.md b/docs/graphs/session-surface.md
new file mode 100644
index 0000000000..b25f85b2d2
--- /dev/null
+++ b/docs/graphs/session-surface.md
@@ -0,0 +1,25 @@
+
+
+# Session Surface And Message Projection
+
+Maintenance mode: curated Mermaid dataflow; exact event/type shapes live in core-data-structures.
+
+This graph separates the append-only log from the derived message surface the next model request sees.
+
+```mermaid
+flowchart LR
+ append["Session.append(type, data)"]
+ log["Append-only SessionEvent log"]
+ surface["SurfaceManager linked list
surfaceOp + sourceEventSeqs"]
+ derive["deriveMessages()"]
+ model["GenerateOptions.messages"]
+ persist["JSONL / SQLite persistence"]
+ replay["load / replay / fork seed"]
+ append --> log
+ log --> surface
+ surface --> derive --> model
+ log --> persist --> replay --> log
+```
+
+See [core-data-structures/session.md](../core-data-structures/session.md) for the full `SessionEventMap`, surface operations, and turn-enclosure invariant.
diff --git a/docs/graphs/snapshot-replay.md b/docs/graphs/snapshot-replay.md
new file mode 100644
index 0000000000..d0ee1c9061
--- /dev/null
+++ b/docs/graphs/snapshot-replay.md
@@ -0,0 +1,24 @@
+
+
+# ACP Snapshot Replay
+
+Maintenance mode: curated Mermaid sequence based on the snapshot test harness.
+
+This graph explains what a snapshot scenario proves: recorded real-model session logs are replayed keylessly, then ACP stdout is normalized and diffed.
+
+```mermaid
+sequenceDiagram
+ participant Recorder as Real API recording
+ participant Fixture as snapshot fixture
+ participant Replay as llm-replay adapter
+ participant ACP as acp-agent subprocess
+ participant Golden as stdout golden
+ Recorder->>Fixture: session.jsonl + workspace inputs
+ Fixture->>Replay: recorded StreamChunk script
+ Replay->>ACP: deterministic llm/stream chunks
+ ACP->>Golden: normalized sessionUpdate stream
+ Golden-->>ACP: diff must be empty
+```
+
+Future pressure from the fs stack: policy rejection scenarios are valuable because they prove both world state and failed tool-card rendering, not just that replay returns text.
diff --git a/docs/graphs/subagent-lineage.md b/docs/graphs/subagent-lineage.md
new file mode 100644
index 0000000000..f687405782
--- /dev/null
+++ b/docs/graphs/subagent-lineage.md
@@ -0,0 +1,27 @@
+
+
+# Subagent And Session Lineage
+
+Maintenance mode: curated Mermaid flow; provider inventory is visible in the generated capability seam graph.
+
+This graph keeps delegation semantics separate from hook observation. A subagent backend creates an ordinary child agent/session through the shared provider registry.
+
+```mermaid
+flowchart TD
+ parent["Parent Agent + Session"]
+ tool["tool-subagent
model-facing name"]
+ registry["ctx.subagents provider registry"]
+ spawn["spawn provider
fresh child session"]
+ fork["fork provider
seeded from completed-turn prefix"]
+ acp["ACP provider
out-of-process child"]
+ child["Child AgentHandle
ordinary Agent lifecycle"]
+ result["SubagentResult returned to tool"]
+ parent --> tool --> registry
+ registry --> spawn --> child
+ registry --> fork --> child
+ registry --> acp --> child
+ child --> result --> parent
+```
+
+The hooks stack adds richer lifecycle observation around child runs; the core ownership rule stays the same: the provider owns the child handle and must dispose it.
diff --git a/docs/graphs/tool-affordance-map.md b/docs/graphs/tool-affordance-map.md
new file mode 100644
index 0000000000..995dee7b08
--- /dev/null
+++ b/docs/graphs/tool-affordance-map.md
@@ -0,0 +1,35 @@
+
+
+# Tool Affordance Map
+
+Maintenance mode: hybrid: tool names/schemas are boot-harvested from shipped tool plugins; required services and shipped aliases are classified in `scripts/gen-doc-graphs.ts` with a completeness guard.
+
+This page connects the model-visible tools to the plugin packages and service seams behind them. For exact JSON Schemas, see [tool-catalog/tools.md](../tool-catalog/tools.md).
+
+```mermaid
+flowchart LR
+ model["Model request tools[]"]
+ toolpkg__deepseek_ai_dsh_tool_bash["tool-bash
bash, bash_kill, bash_output"]
+ model --> toolpkg__deepseek_ai_dsh_tool_bash
+ requires_ctx_tools["ctx.tools"]
+ toolpkg__deepseek_ai_dsh_tool_bash --> requires_ctx_tools
+ requires_ctx_bash["ctx.bash"]
+ toolpkg__deepseek_ai_dsh_tool_bash --> requires_ctx_bash
+ toolpkg__deepseek_ai_dsh_tool_subagent["tool-subagent
subagent"]
+ model --> toolpkg__deepseek_ai_dsh_tool_subagent
+ toolpkg__deepseek_ai_dsh_tool_subagent --> requires_ctx_tools
+ requires_ctx_subagents["ctx.subagents"]
+ toolpkg__deepseek_ai_dsh_tool_subagent --> requires_ctx_subagents
+ toolpkg__deepseek_ai_dsh_tool_todo["tool-todo
todo_write"]
+ model --> toolpkg__deepseek_ai_dsh_tool_todo
+ toolpkg__deepseek_ai_dsh_tool_todo --> requires_ctx_tools
+ requires_owning_Agent_session["owning Agent session"]
+ toolpkg__deepseek_ai_dsh_tool_todo --> requires_owning_Agent_session
+```
+
+| Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Note |
+| --- | --- | --- | --- | --- | --- |
+| `@deepseek-ai/dsh-tool-bash` | `bash`, `bash_kill`, `bash_output` | `ctx.tools`, `ctx.bash` | `tool/call`, `tool/result`, `context/message via agent.inject() for background completion notices` | - | The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam. |
+| `@deepseek-ai/dsh-tool-subagent` | `subagent` | `ctx.tools`, `ctx.subagents` | `tool/call`, `tool/result`, `child session events through the chosen provider` | `subagent`, `subagent_fork` | The default package schema registers subagent; shipped coding/acp configs load it twice to expose spawn and fork backends. |
+| `@deepseek-ai/dsh-tool-todo` | `todo_write` | `ctx.tools`, `owning Agent session` | `tool/call`, `todo/write`, `tool/result` | - | todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan. |
diff --git a/docs/graphs/tool-execution-pipeline.md b/docs/graphs/tool-execution-pipeline.md
new file mode 100644
index 0000000000..7fde39af83
--- /dev/null
+++ b/docs/graphs/tool-execution-pipeline.md
@@ -0,0 +1,30 @@
+
+
+# Tool Execution Pipeline
+
+Maintenance mode: curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs.
+
+This graph shows where policy, hooks, sandboxing, and future filesystem guards fit without changing the loop. The key extension point is the `tools/execute` waterfall.
+
+```mermaid
+flowchart TD
+ model["Assistant message contains tool-call block"]
+ call["Session event: tool/call"]
+ waterfall["ctx.tools.execute()
tools/execute waterfall"]
+ policy["Policy / permission / hooks listener"]
+ body["Registered tool execute() body"]
+ owned["Tool-owned session events
todo/write, future fs policy facts"]
+ result["Session event: tool/result"]
+ ui["UI presentation
presentCall / presentResult"]
+ model --> call --> waterfall
+ waterfall --> policy
+ policy -->|next()| body
+ policy -->|veto / throw| result
+ body --> owned
+ body --> result
+ call --> ui
+ result --> ui
+```
+
+Future pressure from the fs stack: PR #128 snapshots a policy rejection card. The graph keeps the veto path explicit because filesystem read-before-edit checks, permission prompts, and hook bridges all belong on this path.
diff --git a/docs/rfc/README.md b/docs/rfc/README.md
index 981710c4f5..8c5b5ebc9b 100644
--- a/docs/rfc/README.md
+++ b/docs/rfc/README.md
@@ -136,6 +136,7 @@ Do NOT write one for a mechanical or local choice (a variable name, a one-file r
| [Generated cordis events + services catalog](implemented/process/2026-06-20-generated-cordis-catalog.md) | 2026-06-20 |
| [Classify RFCs by kind via path-encoded subdirectories](implemented/process/2026-06-20-rfc-classification.md) | 2026-06-20 |
| [Generated tool-schema catalog (boot-and-harvest)](implemented/process/2026-07-02-tool-schema-catalog.md) | 2026-07-02 |
+| [Documentation graph atlas for maintainers and SDK users](implemented/process/2026-07-03-documentation-graph-atlas.md) | 2026-07-03 |
### Testing
diff --git a/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md b/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md
new file mode 100644
index 0000000000..e707786c14
--- /dev/null
+++ b/docs/rfc/implemented/process/2026-07-03-documentation-graph-atlas.md
@@ -0,0 +1,66 @@
+# RFC: Documentation graph atlas for maintainers and SDK users
+
+Status: implemented (accepted 2026-07-03)
+
+## Context
+
+The repo already had several high-trust documentation surfaces, each on a different axis: [module-graph.md](../../../module-graph.md) is generated from package `peerDependencies`, [cordis-catalog/events-and-services.md](../../../cordis-catalog/events-and-services.md) is generated from Cordis `Events` and `Context` declarations, [tool-catalog/tools.md](../../../tool-catalog/tools.md) is generated by booting shipped tool plugins, and [core-data-structures/](../../../core-data-structures/core.md) uses `ts type-equiv` blocks to keep pasted type definitions synchronized with source.
+
+Those references are accurate, but they are mostly catalogs. A maintainer still has to synthesize the relationships: which packages form a capability seam, which app bundles a concrete spine, which event is durable vs live, where a hook or policy plugin can intercept work, and which model-facing tool depends on which service. An SDK user has the same problem from another angle: "Which package do I install or load for the behavior I want, and which event/service/tool do I extend?"
+
+The pressure is already visible in the open stacks even though this implementation is based on `origin/master`: the hooks stack through PR #129 makes event producer/consumer topology and interception points much more important, while the filesystem stack through PR #128 makes capability seams, policy vetoes, tool presentation, and SDK assembly paths much more important. Graphs based only on today's small bash/todo/subagent surface would become obsolete as soon as those stacks land.
+
+## Decision
+
+Add a generated graph atlas under [docs/graphs/](../../../graphs/README.md), produced by `scripts/gen-doc-graphs.ts` and verified by `pnpm run verify-doc-graphs` as part of `doc-sync`.
+
+The atlas is a relationship layer above the existing catalogs. It does not replace exact references; instead, it links to them and explains how their pieces fit together.
+
+### Maintenance modes
+
+Every graph page declares one maintenance mode:
+
+- **Generated**: all nodes and edges are discovered from source; `--check` fails if the committed artifact is stale.
+- **Hybrid generated**: source discovers the inventory, a small manifest classifies irreducible policy, and a completeness guard fails if discovered items are unclassified.
+- **Curated**: the diagram explains design intent, temporal order, or ownership; it is emitted by the generator so the atlas remains a single regenerated unit, but the content is deliberately authored.
+
+### First shipped atlas
+
+The first atlas ships twelve files: the index plus eleven graph pages.
+
+| Graph | Maintenance mode | Source of truth |
+|---|---|---|
+| [package topology by group](../../../graphs/package-topology.md) | generated | `packages/*/*/package.json` peer dependencies plus package group paths |
+| [capability seams and core services](../../../graphs/capability-seams.md) | hybrid generated | Cordis service declarations plus a role manifest in `gen-doc-graphs.ts` |
+| [app composition](../../../graphs/app-composition.md) | hybrid generated | `examples/*/cordis.yml` plugin lists plus curated app/bundle expansions |
+| [event producer/consumer matrix](../../../graphs/event-producer-consumer.md) | hybrid generated | Cordis event declarations, AST-scanned `ctx.on/emit/parallel/serial/waterfall` sites, and explicit dynamic dispatch overrides |
+| [tool affordance map](../../../graphs/tool-affordance-map.md) | hybrid generated | boot-harvested tool catalog plus a manifest of required services and shipped aliases |
+| [agent turn and step lifecycle](../../../graphs/agent-lifecycle.md) | curated | architecture.md loop lifecycle, Cordis catalog links, and session event semantics |
+| [tool execution pipeline](../../../graphs/tool-execution-pipeline.md) | curated | tool pipeline semantics and the `tools/execute` waterfall |
+| [session surface and message projection](../../../graphs/session-surface.md) | curated | session surface/event-sourcing docs |
+| [subagent and session lineage](../../../graphs/subagent-lineage.md) | curated | subagent seam docs and replay/fork semantics |
+| [plugin disposal and hot reload ownership](../../../graphs/hot-reload-disposal.md) | curated | Cordis fiber/effect ownership conventions |
+| [ACP snapshot replay](../../../graphs/snapshot-replay.md) | curated | snapshot harness behavior |
+
+### Why one generator
+
+Keeping the atlas behind one generator gives reviewers one freshness gate and keeps cross-page terminology synchronized. The tradeoff is that curated diagrams are edited in TypeScript string blocks rather than directly in Markdown. That is acceptable for this first cut because the user-facing artifact is still plain Markdown/Mermaid, and a future change can split the curated pages out if authorship ergonomics matter more than one-command regeneration.
+
+### Completeness guards
+
+The hybrid pages must fail loud when their manifests are stale:
+
+- The capability seam graph imports the Cordis service collector and asserts every discovered harness `ctx.` is classified in `SERVICE_ROLES`, and every classified key still exists.
+- The tool affordance graph boot-harvests the shipped tool catalog and asserts every tool package has `TOOL_PACKAGE_META`.
+- The event producer/consumer matrix labels itself hybrid because subagent lifecycle events deliberately use `ctx.events.dispatch` for per-listener containment; those dynamic edges are explicit overrides rather than invisible omissions.
+
+## Format choices
+
+Use Mermaid for committed diagrams because GitHub renders it in Markdown and it adds no new docs build dependency. Use Markdown tables for dense many-to-many data such as event producer/consumer relationships. Do not adopt PlantUML, hosted diagram services, or generated SVGs until Mermaid becomes the limiting factor.
+
+## Consequences
+
+- Maintainers get visual entry points for topology, seams, event flow, lifecycle, session replay, and snapshot behavior.
+- SDK users get a path from use case to package composition instead of only bottom-up package references.
+- `doc-sync` now includes `verify-doc-graphs`, so graph drift is caught with the other doc freshness gates.
+- Future fs and hooks work has a concrete place to land new complexity: fs should expand the capability and tool graphs, while hooks should expand the event matrix and tool execution pipeline.
diff --git a/package.json b/package.json
index 7d82af4aad..3a35adf5e8 100644
--- a/package.json
+++ b/package.json
@@ -36,10 +36,12 @@
"verify-cordis-catalog": "tsx scripts/gen-cordis-catalog.ts --check",
"gen-tool-catalog": "tsx scripts/gen-tool-catalog.ts",
"verify-tool-catalog": "tsx scripts/gen-tool-catalog.ts --check",
+ "gen-doc-graphs": "tsx scripts/gen-doc-graphs.ts",
+ "verify-doc-graphs": "tsx scripts/gen-doc-graphs.ts --check",
"gen-module-graph": "tsx scripts/gen-module-graph.ts",
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
"constraints": "tsx scripts/check-workspace-constraints.ts",
- "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-tool-catalog && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-rfc-classification && pnpm run verify-type-equiv",
+ "doc-sync": "pnpm run doc-typecheck && pnpm run verify-cordis-catalog && pnpm run verify-tool-catalog && pnpm run verify-doc-graphs && pnpm run verify-md-wrap && pnpm run verify-md-links && pnpm run verify-doc-refs && pnpm run verify-package-paths && pnpm run verify-rfc-classification && pnpm run verify-type-equiv",
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-node-next-types",
"demo:echo": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/echo-agent/cordis.yml",
"demo:coding": "node --expose-internals --import tsx packages/ui/stdio-agent/src/bin.ts examples/coding-agent/cordis.yml",
diff --git a/scripts/gen-doc-graphs.ts b/scripts/gen-doc-graphs.ts
new file mode 100644
index 0000000000..38a385fa62
--- /dev/null
+++ b/scripts/gen-doc-graphs.ts
@@ -0,0 +1,851 @@
+/**
+ * Generate (and verify) the documentation graph atlas in docs/graphs/.
+ *
+ * This is the relationship layer above the existing catalogs:
+ * - module-graph.md answers "which packages depend on which packages?"
+ * - cordis-catalog/ answers "which events and services exist?"
+ * - tool-catalog/ answers "which tools does the model see?"
+ * - docs/graphs/ answers "how do those pieces fit together?"
+ *
+ * Generated pages discover the enumerable facts from source. Hybrid pages use
+ * discovered inventory plus small manifests for policy that source cannot infer
+ * (for example, whether a package is an implementation or consumer in a seam).
+ * Curated pages are still emitted here so the atlas is one regenerated unit,
+ * but their diagrams intentionally explain flow and ownership rather than
+ * pretending to enumerate every source edge.
+ *
+ * `tsx scripts/gen-doc-graphs.ts` -> write docs/graphs/*.md
+ * `tsx scripts/gen-doc-graphs.ts --check` -> exit 1 if any file is stale
+ */
+
+import { existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
+import { dirname, resolve } from 'node:path'
+import ts from 'typescript'
+import { collectEvents, collectServices } from './gen-cordis-catalog.ts'
+import { collectToolCatalog } from './gen-tool-catalog.ts'
+
+const root = resolve(import.meta.dirname, '..')
+const OUT_DIR = 'docs/graphs'
+const SCOPE = '@deepseek-ai/dsh-'
+
+interface PkgJson {
+ name: string
+ peerDependencies?: Record
+}
+
+interface Pkg {
+ short: string
+ name: string
+ group: string
+ rel: string
+ deps: string[]
+}
+
+interface GraphDoc {
+ rel: string
+ content: string
+}
+
+interface ServiceRole {
+ key: string
+ pkg: string
+ title: string
+ mode: 'core' | 'seam' | 'bundle'
+ implementations?: string[]
+ consumers?: string[]
+ note: string
+}
+
+interface ExamplePlugin {
+ id: string
+ name: string
+}
+
+interface EventRelation {
+ dispatchers: Map>
+ listeners: Set
+}
+
+interface ToolPackageMeta {
+ requires: string[]
+ writes: string[]
+ shippedNames?: string[]
+ note: string
+}
+
+const GROUP_ORDER = ['util', 'llm', 'core', 'bash', 'compact', 'subagent', 'session-persistence', 'todo', 'support', 'ui']
+
+const SERVICE_ROLES: ServiceRole[] = [
+ {
+ key: 'llm',
+ pkg: 'llm',
+ title: 'LLM adapter registry',
+ mode: 'seam',
+ implementations: ['llm-deepseek', 'llm-pi-ai', 'llm-replay'],
+ consumers: ['agent-loop', 'compact-basic'],
+ note: 'Adapters register provider implementations; the loop and compaction call the provider-neutral stream service.',
+ },
+ {
+ key: 'sessions',
+ pkg: 'session',
+ title: 'In-memory session store',
+ mode: 'core',
+ consumers: ['agent-loop', 'agent', 'session-persistence', 'subagent-inprocess', 'invariants'],
+ note: 'Owns append-only Session instances and emits the durable session event feed.',
+ },
+ {
+ key: 'sessionPersistence',
+ pkg: 'session-persistence',
+ title: 'Durable session persistence seam',
+ mode: 'seam',
+ implementations: ['session-persistence-jsonl', 'session-persistence-sqlite'],
+ consumers: ['agent-loop', 'acp'],
+ note: 'Backends persist the same SessionEvent vocabulary; apps choose a backend at composition time.',
+ },
+ {
+ key: 'systemPrompt',
+ pkg: 'system-prompt',
+ title: 'System prompt assembly registry',
+ mode: 'core',
+ consumers: ['agent-loop', 'tools'],
+ note: 'Collects prompt sections and model-facing tool schemas for each step.',
+ },
+ {
+ key: 'tools',
+ pkg: 'tools',
+ title: 'Tool registry and execution waterfall',
+ mode: 'core',
+ consumers: ['agent-loop', 'tool-bash', 'tool-subagent', 'tool-todo', 'acp'],
+ note: 'Registers tool definitions, exposes schemas to the prompt, and routes calls through tools/execute.',
+ },
+ {
+ key: 'agents',
+ pkg: 'agent',
+ title: 'Agent registry',
+ mode: 'core',
+ consumers: ['agent-loop', 'acp', 'subagent-inprocess', 'stdio-agent', 'invariants'],
+ note: 'Owns live Agent handles and the create/resume factory seam.',
+ },
+ {
+ key: 'agentLoop',
+ pkg: 'agent-loop',
+ title: 'Concrete loop driver',
+ mode: 'bundle',
+ consumers: ['agent-core'],
+ note: 'The one concrete loop plugin; extension packages depend on dsh-agent events and services, not on this package.',
+ },
+ {
+ key: 'bash',
+ pkg: 'bash',
+ title: 'Bash executor seam',
+ mode: 'seam',
+ implementations: ['bash-local'],
+ consumers: ['tool-bash'],
+ note: 'The model-facing bash tools consume this seam; sandboxed or remote executors can replace bash-local.',
+ },
+ {
+ key: 'compact',
+ pkg: 'compact',
+ title: 'Compaction seam',
+ mode: 'seam',
+ implementations: ['compact-basic'],
+ consumers: ['compact-basic'],
+ note: 'The basic backend currently consumes the pre-step event directly; a model-facing compact tool remains deferred.',
+ },
+ {
+ key: 'subagents',
+ pkg: 'subagent',
+ title: 'Subagent provider registry',
+ mode: 'seam',
+ implementations: ['subagent-spawn', 'subagent-fork', 'subagent-acp', 'subagent-mock'],
+ consumers: ['tool-subagent'],
+ note: 'Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name.',
+ },
+]
+
+const TOOL_PACKAGE_META: Record = {
+ '@deepseek-ai/dsh-tool-bash': {
+ requires: ['ctx.tools', 'ctx.bash'],
+ writes: ['tool/call', 'tool/result', 'context/message via agent.inject() for background completion notices'],
+ note: 'The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam.',
+ },
+ '@deepseek-ai/dsh-tool-subagent': {
+ requires: ['ctx.tools', 'ctx.subagents'],
+ writes: ['tool/call', 'tool/result', 'child session events through the chosen provider'],
+ shippedNames: ['subagent', 'subagent_fork'],
+ note: 'The default package schema registers subagent; shipped coding/acp configs load it twice to expose spawn and fork backends.',
+ },
+ '@deepseek-ai/dsh-tool-todo': {
+ requires: ['ctx.tools', 'owning Agent session'],
+ writes: ['tool/call', 'todo/write', 'tool/result'],
+ note: 'todo_write is session-owned state; UIs render the latest todo/write event as a checklist or ACP plan.',
+ },
+}
+
+const DYNAMIC_EVENT_DISPATCHERS: Array<{ event: string; pkg: string; method: string }> = [
+ // Subagent lifecycle events intentionally bypass ctx.emit and call
+ // ctx.events.dispatch directly so one throwing listener cannot starve later
+ // listeners or strand an already-started child run.
+ { event: 'subagent/start', pkg: 'subagent', method: 'events.dispatch' },
+ { event: 'subagent/end', pkg: 'subagent', method: 'events.dispatch' },
+]
+
+function generatedHeader(title: string, source: string): string[] {
+ return [
+ '',
+ '',
+ `# ${title}`,
+ '',
+ `Maintenance mode: ${source}.`,
+ '',
+ ]
+}
+
+function collectPackages(): Pkg[] {
+ const pkgs: Pkg[] = []
+ for (const rel of globSync('packages/*/*/package.json', { cwd: root }).sort()) {
+ const json = JSON.parse(readFileSync(resolve(root, rel), 'utf8')) as PkgJson
+ if (!json.name.startsWith(SCOPE)) continue
+ const [, group, leaf] = rel.split('/')
+ if (group === undefined || leaf === undefined) throw new Error(`gen-doc-graphs: unexpected package path ${rel}`)
+ const deps = Object.keys(json.peerDependencies ?? {})
+ .filter(dep => dep.startsWith(SCOPE))
+ .map(dep => dep.slice(SCOPE.length))
+ .sort()
+ pkgs.push({
+ short: json.name.slice(SCOPE.length),
+ name: json.name,
+ group,
+ rel: dirname(rel),
+ deps,
+ })
+ }
+ return topoSort(pkgs)
+}
+
+function topoSort(pkgs: Pkg[]): Pkg[] {
+ const remaining = new Map(pkgs.map(p => [p.short, p]))
+ const placed = new Set()
+ const out: Pkg[] = []
+ while (remaining.size > 0) {
+ const ready = [...remaining.values()]
+ .filter(pkg => pkg.deps.every(dep => placed.has(dep)))
+ .sort(comparePackages)
+ if (ready.length === 0) throw new Error(`gen-doc-graphs: dependency cycle among ${[...remaining.keys()].join(', ')}`)
+ for (const pkg of ready) {
+ out.push(pkg)
+ placed.add(pkg.short)
+ remaining.delete(pkg.short)
+ }
+ }
+ return out
+}
+
+function comparePackages(a: Pkg, b: Pkg): number {
+ const groupA = GROUP_ORDER.indexOf(a.group)
+ const groupB = GROUP_ORDER.indexOf(b.group)
+ const normA = groupA === -1 ? Number.MAX_SAFE_INTEGER : groupA
+ const normB = groupB === -1 ? Number.MAX_SAFE_INTEGER : groupB
+ return normA - normB || a.group.localeCompare(b.group) || a.short.localeCompare(b.short)
+}
+
+function nodeId(prefix: string, value: string): string {
+ return `${prefix}_${value.replace(/[^a-zA-Z0-9_]/g, '_')}`
+}
+
+function escLabel(value: string): string {
+ return value.replace(/"/g, '\\"')
+}
+
+function pkgLink(pkg: Pkg | undefined, fallback: string): string {
+ return pkg ? `[\`${pkg.short}\`](../../${pkg.rel})` : `\`${fallback}\``
+}
+
+function pkgList(names: string[] | undefined, pkgsByShort: Map): string {
+ if (!names || names.length === 0) return '-'
+ return names.map(name => pkgLink(pkgsByShort.get(name), name)).join(', ')
+}
+
+function codeList(values: string[]): string {
+ return values.length ? values.map(v => `\`${v}\``).join(', ') : '-'
+}
+
+function tableCell(value: string): string {
+ return value.replace(/\|/g, '\\|').replace(/\n/g, '
')
+}
+
+function renderMermaidPackageNode(pkg: Pkg): string {
+ return ` ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`
+}
+
+function renderPackageTopology(pkgs: Pkg[]): string {
+ const lines = generatedHeader('Package Topology By Group', 'generated from `packages/*/*/package.json` peer dependencies plus package group paths')
+ lines.push(
+ 'This graph complements [module-graph.md](../module-graph.md): it keeps the same canonical peer-dependency edge source, but clusters packages by the `packages//` hierarchy so layering and capability families are easier to scan.',
+ '',
+ '```mermaid',
+ 'flowchart TD',
+ )
+ const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((a, b) => {
+ const ia = GROUP_ORDER.indexOf(a)
+ const ib = GROUP_ORDER.indexOf(b)
+ const na = ia === -1 ? Number.MAX_SAFE_INTEGER : ia
+ const nb = ib === -1 ? Number.MAX_SAFE_INTEGER : ib
+ return na - nb || a.localeCompare(b)
+ })
+ for (const group of groups) {
+ lines.push(` subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`)
+ for (const pkg of pkgs.filter(p => p.group === group).sort((a, b) => a.short.localeCompare(b.short))) {
+ lines.push(renderMermaidPackageNode(pkg))
+ }
+ lines.push(' end')
+ }
+ for (const pkg of pkgs) {
+ for (const dep of pkg.deps) lines.push(` ${nodeId('pkg', pkg.short)} --> ${nodeId('pkg', dep)}`)
+ }
+ lines.push('```', '', '| Package | Group | Depends on |', '| --- | --- | --- |')
+ const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
+ for (const pkg of pkgs) {
+ lines.push(`| ${pkgLink(pkg, pkg.short)} | \`${pkg.group}\` | ${pkg.deps.length ? pkg.deps.map(dep => pkgLink(byShort.get(dep), dep)).join(', ') : '-'} |`)
+ }
+ lines.push('')
+ return lines.join('\n')
+}
+
+function assertServiceRolesComplete(): void {
+ const discovered = new Set(collectServices().map(service => service.key))
+ const classified = new Set(SERVICE_ROLES.map(role => role.key))
+ const missing = [...discovered].filter(key => !classified.has(key)).sort()
+ const stale = [...classified].filter(key => !discovered.has(key)).sort()
+ if (missing.length || stale.length) {
+ throw new Error([
+ missing.length ? `missing service role classification: ${missing.join(', ')}` : '',
+ stale.length ? `stale service role classification: ${stale.join(', ')}` : '',
+ ].filter(Boolean).join('; '))
+ }
+}
+
+function renderCapabilitySeams(pkgs: Pkg[]): string {
+ assertServiceRolesComplete()
+ const pkgsByShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
+ const nodes = new Map()
+ const edges = new Set()
+ const addNode = (id: string, label: string): void => {
+ if (!nodes.has(id)) nodes.set(id, ` ${id}["${escLabel(label)}"]`)
+ }
+ const addEdge = (from: string, to: string): void => { edges.add(` ${from} --> ${to}`) }
+ const lines = generatedHeader('Capability Seams And Core Services', 'hybrid: services are discovered from Cordis declarations; interface/implementation/consumer roles are classified in `scripts/gen-doc-graphs.ts` with a completeness guard')
+ lines.push(
+ 'A service can be a core spine service, a swappable capability seam, or a bundle/composition point. The graph shows the package that owns the service declaration, known implementation packages, and packages that consume the service directly.',
+ '',
+ '```mermaid',
+ 'flowchart LR',
+ )
+ for (const role of SERVICE_ROLES) {
+ const svc = nodeId('svc', role.key)
+ const owner = nodeId('pkg', role.pkg)
+ addNode(owner, role.pkg)
+ addNode(svc, `ctx.${role.key}
${role.title}`)
+ addEdge(owner, svc)
+ for (const impl of role.implementations ?? []) {
+ addNode(nodeId('pkg', impl), impl)
+ addEdge(nodeId('pkg', impl), svc)
+ }
+ for (const consumer of role.consumers ?? []) {
+ addNode(nodeId('pkg', consumer), consumer)
+ addEdge(svc, nodeId('pkg', consumer))
+ }
+ }
+ lines.push(...nodes.values(), ...[...edges].sort())
+ lines.push('```', '', '| ctx key | Role | Owner | Implementations | Direct consumers | Note |', '| --- | --- | --- | --- | --- | --- |')
+ for (const role of SERVICE_ROLES) {
+ lines.push(`| \`ctx.${role.key}\` | \`${role.mode}\` | ${pkgLink(pkgsByShort.get(role.pkg), role.pkg)} | ${pkgList(role.implementations, pkgsByShort)} | ${pkgList(role.consumers, pkgsByShort)} | ${tableCell(role.note)} |`)
+ }
+ lines.push('')
+ return lines.join('\n')
+}
+
+function parseExampleCordis(rel: string): ExamplePlugin[] {
+ const text = readFileSync(resolve(root, rel), 'utf8')
+ const plugins: ExamplePlugin[] = []
+ let current: { id: string; name?: string } | null = null
+ const flush = (): void => {
+ if (current?.name) plugins.push({ id: current.id, name: current.name })
+ }
+ for (const line of text.split('\n')) {
+ const id = /^-\s+id:\s+(.+?)\s*$/.exec(line)
+ if (id?.[1] !== undefined) {
+ flush()
+ current = { id: stripYamlScalar(id[1]) }
+ continue
+ }
+ const name = /^\s+name:\s+(.+?)\s*$/.exec(line)
+ if (name?.[1] !== undefined && current) current.name = stripYamlScalar(name[1])
+ }
+ flush()
+ return plugins
+}
+
+function stripYamlScalar(value: string): string {
+ return value.trim().replace(/^['"]|['"]$/g, '')
+}
+
+function renderAppComposition(): string {
+ const examples = [
+ { id: 'echo', label: 'examples/echo-agent', config: 'examples/echo-agent/cordis.yml' },
+ { id: 'coding', label: 'examples/coding-agent', config: 'examples/coding-agent/cordis.yml' },
+ { id: 'acp', label: 'examples/acp-agent', config: 'examples/acp-agent/cordis.yml' },
+ ]
+ const lines = generatedHeader('App Composition', 'hybrid: leaf plugin lists are parsed from `examples/*/cordis.yml`; bundle expansions are curated from app package source')
+ lines.push(
+ 'This graph is for SDK users asking which pieces a runnable agent loads. Leaf configs choose adapters and optional product tools; app packages provide the front door; `dsh-agent-core` bundles the providerless spine.',
+ '',
+ '```mermaid',
+ 'flowchart LR',
+ )
+ const bundleTargets: Record = {
+ '@deepseek-ai/dsh-stdio-agent': nodeId('bundle', 'stdio'),
+ '@deepseek-ai/dsh-acp-agent': nodeId('bundle', 'acp_agent'),
+ }
+ for (const example of examples) {
+ lines.push(` subgraph ${nodeId('example', example.id)}["${escLabel(example.label)}"]`)
+ lines.push(` ${nodeId('cfg', example.id)}["cordis.yml"]`)
+ for (const plugin of parseExampleCordis(example.config)) {
+ const pluginNode = nodeId(`plugin_${example.id}`, plugin.id)
+ lines.push(` ${pluginNode}["${escLabel(plugin.id)}
${escLabel(plugin.name)}"]`)
+ lines.push(` ${nodeId('cfg', example.id)} --> ${pluginNode}`)
+ const bundle = bundleTargets[plugin.name]
+ if (bundle !== undefined) lines.push(` ${pluginNode} --> ${bundle}`)
+ }
+ lines.push(' end')
+ }
+ lines.push(
+ ` ${nodeId('bundle', 'stdio')}["@deepseek-ai/dsh-stdio-agent"] --> ${nodeId('bundle', 'agent_core')}["@deepseek-ai/dsh-agent-core"]`,
+ ` ${nodeId('bundle', 'stdio')} --> ${nodeId('bundle', 'jsonl')}["@deepseek-ai/dsh-session-persistence-jsonl"]`,
+ ` ${nodeId('bundle', 'stdio')} --> ${nodeId('bundle', 'ui_stdio')}["@deepseek-ai/dsh-ui-stdio"]`,
+ ` ${nodeId('bundle', 'acp_agent')}["@deepseek-ai/dsh-acp-agent"] --> ${nodeId('bundle', 'agent_core')}`,
+ ` ${nodeId('bundle', 'acp_agent')} --> ${nodeId('bundle', 'jsonl')}`,
+ ` ${nodeId('bundle', 'acp_agent')} --> ${nodeId('bundle', 'acp')}["@deepseek-ai/dsh-acp"]`,
+ ` ${nodeId('bundle', 'agent_core')} --> ${nodeId('spine', 'llm')}["ctx.llm"]`,
+ ` ${nodeId('bundle', 'agent_core')} --> ${nodeId('spine', 'sessions')}["ctx.sessions"]`,
+ ` ${nodeId('bundle', 'agent_core')} --> ${nodeId('spine', 'tools')}["ctx.tools + tool-bash"]`,
+ ` ${nodeId('bundle', 'agent_core')} --> ${nodeId('spine', 'loop')}["ctx.agents + ctx.agentLoop"]`,
+ '```',
+ '',
+ '| Example | Parsed plugin ids | Config |',
+ '| --- | --- | --- |',
+ )
+ for (const example of examples) {
+ const plugins = parseExampleCordis(example.config)
+ lines.push(`| \`${example.label}\` | ${plugins.map(plugin => `\`${plugin.id}\``).join(', ')} | [\`${example.config}\`](../../${example.config}) |`)
+ }
+ lines.push('')
+ return lines.join('\n')
+}
+
+function collectEventRelations(): Map {
+ const out = new Map()
+ const ensure = (event: string): EventRelation => {
+ const existing = out.get(event)
+ if (existing) return existing
+ const next = { dispatchers: new Map>(), listeners: new Set() }
+ out.set(event, next)
+ return next
+ }
+ for (const rel of globSync('packages/*/*/src/**/*.ts', { cwd: root }).sort()) {
+ const [, , leaf] = rel.split('/')
+ if (leaf === undefined) continue
+ const text = readFileSync(resolve(root, rel), 'utf8')
+ const sf = ts.createSourceFile(rel, text, ts.ScriptTarget.Latest, true)
+ const visit = (node: ts.Node): void => {
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
+ const method = node.expression.name.text
+ if (!isCordisContextReceiver(node.expression, sf)) {
+ ts.forEachChild(node, visit)
+ return
+ }
+ if (method === 'on') {
+ const event = eventArg(node.arguments, method)
+ if (event) ensure(event).listeners.add(leaf)
+ } else if (method === 'emit' || method === 'parallel' || method === 'serial' || method === 'waterfall') {
+ const event = eventArg(node.arguments, method)
+ if (event) {
+ const relation = ensure(event)
+ const methods = relation.dispatchers.get(leaf) ?? new Set()
+ methods.add(method)
+ relation.dispatchers.set(leaf, methods)
+ }
+ }
+ }
+ ts.forEachChild(node, visit)
+ }
+ visit(sf)
+ }
+ for (const entry of DYNAMIC_EVENT_DISPATCHERS) {
+ const relation = ensure(entry.event)
+ const methods = relation.dispatchers.get(entry.pkg) ?? new Set()
+ methods.add(entry.method)
+ relation.dispatchers.set(entry.pkg, methods)
+ }
+ return out
+}
+
+function isCordisContextReceiver(expr: ts.PropertyAccessExpression, sf: ts.SourceFile): boolean {
+ const target = expr.expression.getText(sf)
+ return target === 'ctx' || target === 'this.ctx'
+}
+
+function eventArg(args: ts.NodeArray, method: string): string | undefined {
+ if (method === 'waterfall') {
+ const arg = args.find(ts.isStringLiteralLike)
+ return arg?.text
+ }
+ const first = args[0]
+ return first && ts.isStringLiteralLike(first) ? first.text : undefined
+}
+
+function relationPackages(map: Map>, pkgsByShort: Map): string {
+ if (map.size === 0) return '-'
+ return [...map.entries()]
+ .sort(([a], [b]) => a.localeCompare(b))
+ .map(([pkg, methods]) => `${pkgLink(pkgsByShort.get(pkg), pkg)} (${[...methods].sort().map(m => `\`${m}\``).join(', ')})`)
+ .join(', ')
+}
+
+function listenerPackages(listeners: Set, pkgsByShort: Map): string {
+ if (listeners.size === 0) return '-'
+ return [...listeners].sort().map(pkg => pkgLink(pkgsByShort.get(pkg), pkg)).join(', ')
+}
+
+function renderEventRelations(pkgs: Pkg[]): string {
+ const events = collectEvents()
+ const relations = collectEventRelations()
+ const pkgsByShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
+ const lines = generatedHeader('Event Producer And Consumer Matrix', 'hybrid generated: Cordis event declarations and most producer/listener edges are AST-scanned; dynamic dispatch sites are classified in `scripts/gen-doc-graphs.ts`')
+ lines.push(
+ 'This matrix shows which packages dispatch each harness-owned event and which packages listen to it. It is intentionally a table rather than one large graph: events are many-to-many, and dense relation data is easier to review in rows. Dynamic dispatch overrides cover sites that deliberately bypass `ctx.emit`, such as subagent lifecycle containment.',
+ '',
+ '| Event | Mode | Declared in | Dispatchers | Listeners |',
+ '| --- | --- | --- | --- | --- |',
+ )
+ for (const event of [...events].sort((a, b) => a.name.localeCompare(b.name))) {
+ const relation = relations.get(event.name) ?? { dispatchers: new Map>(), listeners: new Set() }
+ lines.push(`| \`${event.name}\` | \`${event.mode}\` | [\`${event.source}\`](../../${event.source.split(':')[0]}) | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`)
+ }
+ const declared = new Set(events.map(event => event.name))
+ const extra = [...relations.keys()].filter(event => !declared.has(event)).sort()
+ if (extra.length > 0) {
+ lines.push('', '## Non-harness or undeclared event strings seen in package source', '', '| Event string | Dispatchers | Listeners |', '| --- | --- | --- |')
+ for (const event of extra) {
+ const relation = relations.get(event)
+ if (!relation) continue
+ lines.push(`| \`${event}\` | ${relationPackages(relation.dispatchers, pkgsByShort)} | ${listenerPackages(relation.listeners, pkgsByShort)} |`)
+ }
+ }
+ lines.push('')
+ return lines.join('\n')
+}
+
+async function renderToolAffordance(): Promise {
+ const catalog = await collectToolCatalog()
+ const lines = generatedHeader('Tool Affordance Map', 'hybrid: tool names/schemas are boot-harvested from shipped tool plugins; required services and shipped aliases are classified in `scripts/gen-doc-graphs.ts` with a completeness guard')
+ for (const entry of catalog) {
+ if (!TOOL_PACKAGE_META[entry.pkg]) {
+ throw new Error(`gen-doc-graphs: tool package ${entry.pkg} is missing TOOL_PACKAGE_META classification`)
+ }
+ }
+ lines.push(
+ 'This page connects the model-visible tools to the plugin packages and service seams behind them. For exact JSON Schemas, see [tool-catalog/tools.md](../tool-catalog/tools.md).',
+ '',
+ '```mermaid',
+ 'flowchart LR',
+ ' model["Model request tools[]"]',
+ )
+ const requirementNodes = new Set()
+ for (const entry of catalog) {
+ const meta = TOOL_PACKAGE_META[entry.pkg]
+ if (!meta) continue
+ const packageNode = nodeId('toolpkg', entry.pkg)
+ const names = entry.schemas.map(schema => schema.name).join(', ')
+ lines.push(` ${packageNode}["${escLabel(entry.pkg.replace(SCOPE, ''))}
${escLabel(names)}"]`)
+ lines.push(` model --> ${packageNode}`)
+ for (const req of meta.requires) {
+ const reqNode = nodeId('requires', req)
+ if (!requirementNodes.has(reqNode)) {
+ lines.push(` ${reqNode}["${escLabel(req)}"]`)
+ requirementNodes.add(reqNode)
+ }
+ lines.push(` ${packageNode} --> ${reqNode}`)
+ }
+ }
+ lines.push('```', '', '| Tool package | Model-visible names | Requires | Writes / affects | Shipped aliases | Note |', '| --- | --- | --- | --- | --- | --- |')
+ for (const entry of catalog) {
+ const meta = TOOL_PACKAGE_META[entry.pkg]
+ if (!meta) continue
+ lines.push(`| \`${entry.pkg}\` | ${codeList(entry.schemas.map(schema => schema.name))} | ${codeList(meta.requires)} | ${codeList(meta.writes)} | ${codeList(meta.shippedNames ?? [])} | ${tableCell(meta.note)} |`)
+ }
+ lines.push('')
+ return lines.join('\n')
+}
+
+function renderLifecycle(): string {
+ return [
+ ...generatedHeader('Agent Turn And Step Lifecycle', 'curated Mermaid sequence; exact event signatures live in the generated Cordis catalog'),
+ 'This sequence is the visual companion to [architecture.md](../architecture.md#loop-lifecycle-session--turn--step). It shows the durable session event path separately from live `agent/*` notifications.',
+ '',
+ '```mermaid',
+ 'sequenceDiagram',
+ ' participant User',
+ ' participant Agent',
+ ' participant Loop',
+ ' participant Prompt as ctx.systemPrompt',
+ ' participant LLM as ctx.llm',
+ ' participant Tools as ctx.tools',
+ ' participant Session',
+ ' participant Persistence',
+ ' User->>Agent: send(content)',
+ ' Agent->>Loop: queued work wakes driver',
+ ' Loop->>Session: turn/start + user/message',
+ ' Loop-->>User: agent/turn-start',
+ ' Loop->>Prompt: system-prompt/assemble waterfall',
+ ' Loop-->>Loop: agent/pre-step serial checkpoint',
+ ' Loop->>Session: step/start',
+ ' Loop->>LLM: agent/request waterfall, then llm/stream waterfall',
+ ' LLM-->>Loop: StreamChunk*',
+ ' Loop->>Session: assistant/chunk*',
+ ' Loop-->>User: agent/stream-chunk* (master live mirror)',
+ ' Loop->>Session: assistant/message',
+ ' Loop->>Tools: tools/execute waterfall for each tool-call',
+ ' Tools-->>Session: tool-owned events when applicable',
+ ' Loop->>Session: tool/result',
+ ' Loop-->>Loop: agent/turn-continuation waterfall',
+ ' Loop->>Session: turn/end',
+ ' Loop->>Persistence: session/flush parallel checkpoint',
+ ' Loop-->>User: agent/status idle',
+ '```',
+ '',
+ 'Future pressure from the hooks stack: PR #129 removes the live `agent/stream-chunk` mirror and leaves durable `assistant/chunk` on `session/event` as the authoritative token stream. Consumers that need replayable transcript data should already treat `session/event` as the load-bearing path.',
+ '',
+ ].join('\n')
+}
+
+function renderToolPipeline(): string {
+ return [
+ ...generatedHeader('Tool Execution Pipeline', 'curated Mermaid flow; exact tool schemas and event signatures live in generated catalogs'),
+ 'This graph shows where policy, hooks, sandboxing, and future filesystem guards fit without changing the loop. The key extension point is the `tools/execute` waterfall.',
+ '',
+ '```mermaid',
+ 'flowchart TD',
+ ' model["Assistant message contains tool-call block"]',
+ ' call["Session event: tool/call"]',
+ ' waterfall["ctx.tools.execute()
tools/execute waterfall"]',
+ ' policy["Policy / permission / hooks listener"]',
+ ' body["Registered tool execute() body"]',
+ ' owned["Tool-owned session events
todo/write, future fs policy facts"]',
+ ' result["Session event: tool/result"]',
+ ' ui["UI presentation
presentCall / presentResult"]',
+ ' model --> call --> waterfall',
+ ' waterfall --> policy',
+ ' policy -->|next()| body',
+ ' policy -->|veto / throw| result',
+ ' body --> owned',
+ ' body --> result',
+ ' call --> ui',
+ ' result --> ui',
+ '```',
+ '',
+ 'Future pressure from the fs stack: PR #128 snapshots a policy rejection card. The graph keeps the veto path explicit because filesystem read-before-edit checks, permission prompts, and hook bridges all belong on this path.',
+ '',
+ ].join('\n')
+}
+
+function renderSessionSurface(): string {
+ return [
+ ...generatedHeader('Session Surface And Message Projection', 'curated Mermaid dataflow; exact event/type shapes live in core-data-structures'),
+ 'This graph separates the append-only log from the derived message surface the next model request sees.',
+ '',
+ '```mermaid',
+ 'flowchart LR',
+ ' append["Session.append(type, data)"]',
+ ' log["Append-only SessionEvent log"]',
+ ' surface["SurfaceManager linked list
surfaceOp + sourceEventSeqs"]',
+ ' derive["deriveMessages()"]',
+ ' model["GenerateOptions.messages"]',
+ ' persist["JSONL / SQLite persistence"]',
+ ' replay["load / replay / fork seed"]',
+ ' append --> log',
+ ' log --> surface',
+ ' surface --> derive --> model',
+ ' log --> persist --> replay --> log',
+ '```',
+ '',
+ 'See [core-data-structures/session.md](../core-data-structures/session.md) for the full `SessionEventMap`, surface operations, and turn-enclosure invariant.',
+ '',
+ ].join('\n')
+}
+
+function renderSubagentLineage(): string {
+ return [
+ ...generatedHeader('Subagent And Session Lineage', 'curated Mermaid flow; provider inventory is visible in the generated capability seam graph'),
+ 'This graph keeps delegation semantics separate from hook observation. A subagent backend creates an ordinary child agent/session through the shared provider registry.',
+ '',
+ '```mermaid',
+ 'flowchart TD',
+ ' parent["Parent Agent + Session"]',
+ ' tool["tool-subagent
model-facing name"]',
+ ' registry["ctx.subagents provider registry"]',
+ ' spawn["spawn provider
fresh child session"]',
+ ' fork["fork provider
seeded from completed-turn prefix"]',
+ ' acp["ACP provider
out-of-process child"]',
+ ' child["Child AgentHandle
ordinary Agent lifecycle"]',
+ ' result["SubagentResult returned to tool"]',
+ ' parent --> tool --> registry',
+ ' registry --> spawn --> child',
+ ' registry --> fork --> child',
+ ' registry --> acp --> child',
+ ' child --> result --> parent',
+ '```',
+ '',
+ 'The hooks stack adds richer lifecycle observation around child runs; the core ownership rule stays the same: the provider owns the child handle and must dispose it.',
+ '',
+ ].join('\n')
+}
+
+function renderHotReload(): string {
+ return [
+ ...generatedHeader('Plugin Disposal And Hot Reload Ownership', 'curated Mermaid flow based on Cordis fiber/effect conventions'),
+ 'This graph is a maintainer checklist for plugin authors: registrations are effects, service injection gates activation, and owned handles must be disposed by their owner.',
+ '',
+ '```mermaid',
+ 'flowchart TD',
+ ' plugin["ctx.plugin(plugin) creates fiber"]',
+ ' inject["static inject gates activation"]',
+ ' service["ctx.provide / Service constructor"]',
+ ' effects["ctx.effect registrations
events, tools, adapters, timers"]',
+ ' reload["HMR / fiber.dispose()"]',
+ ' disposers["Run disposers in owner fiber"]',
+ ' quiescence["Owned AgentHandle.dispose()
or service teardown awaits quiescence"]',
+ ' plugin --> inject --> service',
+ ' inject --> effects',
+ ' reload --> disposers --> quiescence',
+ '```',
+ '',
+ 'Hook bridges and SDK plugins increase the number of long-lived listeners, so this ownership graph should stay small and visible.',
+ '',
+ ].join('\n')
+}
+
+function renderSnapshotReplay(): string {
+ return [
+ ...generatedHeader('ACP Snapshot Replay', 'curated Mermaid sequence based on the snapshot test harness'),
+ 'This graph explains what a snapshot scenario proves: recorded real-model session logs are replayed keylessly, then ACP stdout is normalized and diffed.',
+ '',
+ '```mermaid',
+ 'sequenceDiagram',
+ ' participant Recorder as Real API recording',
+ ' participant Fixture as snapshot fixture',
+ ' participant Replay as llm-replay adapter',
+ ' participant ACP as acp-agent subprocess',
+ ' participant Golden as stdout golden',
+ ' Recorder->>Fixture: session.jsonl + workspace inputs',
+ ' Fixture->>Replay: recorded StreamChunk script',
+ ' Replay->>ACP: deterministic llm/stream chunks',
+ ' ACP->>Golden: normalized sessionUpdate stream',
+ ' Golden-->>ACP: diff must be empty',
+ '```',
+ '',
+ 'Future pressure from the fs stack: policy rejection scenarios are valuable because they prove both world state and failed tool-card rendering, not just that replay returns text.',
+ '',
+ ].join('\n')
+}
+
+async function renderDocs(): Promise {
+ const pkgs = collectPackages()
+ const docs: GraphDoc[] = [
+ { rel: `${OUT_DIR}/package-topology.md`, content: renderPackageTopology(pkgs) },
+ { rel: `${OUT_DIR}/capability-seams.md`, content: renderCapabilitySeams(pkgs) },
+ { rel: `${OUT_DIR}/app-composition.md`, content: renderAppComposition() },
+ { rel: `${OUT_DIR}/event-producer-consumer.md`, content: renderEventRelations(pkgs) },
+ { rel: `${OUT_DIR}/tool-affordance-map.md`, content: await renderToolAffordance() },
+ { rel: `${OUT_DIR}/agent-lifecycle.md`, content: renderLifecycle() },
+ { rel: `${OUT_DIR}/tool-execution-pipeline.md`, content: renderToolPipeline() },
+ { rel: `${OUT_DIR}/session-surface.md`, content: renderSessionSurface() },
+ { rel: `${OUT_DIR}/subagent-lineage.md`, content: renderSubagentLineage() },
+ { rel: `${OUT_DIR}/hot-reload-disposal.md`, content: renderHotReload() },
+ { rel: `${OUT_DIR}/snapshot-replay.md`, content: renderSnapshotReplay() },
+ ]
+ docs.unshift({ rel: `${OUT_DIR}/README.md`, content: renderIndex(docs) })
+ return docs
+}
+
+function renderIndex(docs: GraphDoc[]): string {
+ const labels: Record = {
+ 'package-topology.md': 'package topology by group',
+ 'capability-seams.md': 'capability seams and core services',
+ 'app-composition.md': 'app composition',
+ 'event-producer-consumer.md': 'event producer/consumer matrix',
+ 'tool-affordance-map.md': 'tool affordance map',
+ 'agent-lifecycle.md': 'agent turn and step lifecycle',
+ 'tool-execution-pipeline.md': 'tool execution pipeline',
+ 'session-surface.md': 'session surface and message projection',
+ 'subagent-lineage.md': 'subagent and session lineage',
+ 'hot-reload-disposal.md': 'plugin disposal and hot reload ownership',
+ 'snapshot-replay.md': 'ACP snapshot replay',
+ }
+ const modes: Record = {
+ 'package-topology.md': 'generated',
+ 'capability-seams.md': 'hybrid generated',
+ 'app-composition.md': 'hybrid generated',
+ 'event-producer-consumer.md': 'hybrid generated',
+ 'tool-affordance-map.md': 'hybrid generated',
+ 'agent-lifecycle.md': 'curated',
+ 'tool-execution-pipeline.md': 'curated',
+ 'session-surface.md': 'curated',
+ 'subagent-lineage.md': 'curated',
+ 'hot-reload-disposal.md': 'curated',
+ 'snapshot-replay.md': 'curated',
+ }
+ return [
+ ...generatedHeader('Documentation Graph Atlas', 'mixed: each linked page declares generated, hybrid, or curated mode'),
+ 'The graph atlas is the relationship layer above the generated catalogs. Use it to navigate package topology, capability seams, event flow, model-facing tools, and runtime lifecycle paths. Exact signatures and type shapes still live in [cordis-catalog/](../cordis-catalog/events-and-services.md), [tool-catalog/](../tool-catalog/tools.md), and [core-data-structures/](../core-data-structures/core.md).',
+ '',
+ 'The process decision behind this atlas is recorded in [the documentation graph atlas RFC](../rfc/implemented/process/2026-07-03-documentation-graph-atlas.md).',
+ '',
+ '| Graph | Mode |',
+ '| --- | --- |',
+ ...docs.map((doc) => {
+ const file = doc.rel.split('/').at(-1) ?? doc.rel
+ return `| [${labels[file] ?? file}](${file}) | \`${modes[file] ?? 'generated'}\` |`
+ }),
+ '',
+ 'Regenerate with `pnpm run gen-doc-graphs`; verify freshness with `pnpm run verify-doc-graphs`.',
+ '',
+ ].join('\n')
+}
+
+async function main(): Promise {
+ const docs = await renderDocs()
+ if (process.argv.includes('--check')) {
+ const stale: string[] = []
+ for (const doc of docs) {
+ const abs = resolve(root, doc.rel)
+ const committed = existsSync(abs) ? readFileSync(abs, 'utf8') : null
+ if (committed !== doc.content) stale.push(doc.rel)
+ }
+ if (stale.length === 0) {
+ console.log(`gen-doc-graphs: ${docs.length} graph doc(s) are up to date.`)
+ return
+ }
+ console.error(`gen-doc-graphs: stale graph doc(s): ${stale.join(', ')}. Run \`pnpm run gen-doc-graphs\` and commit the result.`)
+ process.exit(1)
+ }
+
+ mkdirSync(resolve(root, OUT_DIR), { recursive: true })
+ for (const doc of docs) writeFileSync(resolve(root, doc.rel), doc.content)
+ console.log(`gen-doc-graphs: wrote ${docs.length} graph doc(s).`)
+}
+
+if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
+ await main()
+}