summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.dispatch/rules/kernel-purity.md6
-rw-r--r--.dispatch/rules/no-internal-mocks.md7
-rw-r--r--.dispatch/rules/one-owner.md6
-rw-r--r--.dispatch/rules/pure-core.md6
-rw-r--r--.dispatch/rules/typed-handles.md7
-rw-r--r--.env.example5
-rw-r--r--.gitignore14
-rw-r--r--AGENTS.md67
-rw-r--r--GLOSSARY.md27
-rw-r--r--README.md19
-rw-r--r--biome.json8
-rw-r--r--bun.lock250
-rw-r--r--dispatch.toml7
-rw-r--r--notes/restructure-plan.md1451
-rw-r--r--package.json21
-rw-r--r--packages/kernel/package.json8
-rw-r--r--packages/kernel/src/index.ts1
-rw-r--r--packages/kernel/tsconfig.json5
-rw-r--r--tsconfig.base.json22
-rw-r--r--tsconfig.json4
-rw-r--r--vitest.config.ts7
21 files changed, 1948 insertions, 0 deletions
diff --git a/.dispatch/rules/kernel-purity.md b/.dispatch/rules/kernel-purity.md
new file mode 100644
index 0000000..cdd975b
--- /dev/null
+++ b/.dispatch/rules/kernel-purity.md
@@ -0,0 +1,6 @@
+# Rule: kernel purity
+
+The kernel touches NO I/O and names NO concrete feature.
+Forbidden in `packages/kernel`: `node:fs`, `bun:sqlite`, `node:child_process`,
+`fetch`/network, and any import of a concrete tool/provider/extension.
+Need an effect? It belongs in an extension, injected through a contract.
diff --git a/.dispatch/rules/no-internal-mocks.md b/.dispatch/rules/no-internal-mocks.md
new file mode 100644
index 0000000..20c1f99
--- /dev/null
+++ b/.dispatch/rules/no-internal-mocks.md
@@ -0,0 +1,7 @@
+# Rule: no internal mocks
+
+A unit test that mocks one of OUR OWN modules (`@dispatch/*`) is a DESIGN BUG,
+not a test to write. Move the decision logic to a pure function and inject the
+effect, then test the pure function directly.
+Mocking the OUTERMOST edge (real network, real clock) is the only allowed mock.
+Kernel + pure-core packages must reach ZERO internal mocks.
diff --git a/.dispatch/rules/one-owner.md b/.dispatch/rules/one-owner.md
new file mode 100644
index 0000000..04fec56
--- /dev/null
+++ b/.dispatch/rules/one-owner.md
@@ -0,0 +1,6 @@
+# Rule: one owner per unit
+
+You may ONLY edit the files you were explicitly assigned. No exceptions.
+To change another unit, do NOT edit it — write the requested change into your
+report (`reports/<unit>.md`) for the orchestrator to dispatch.
+You see other units' CONTRACTS, never their implementation.
diff --git a/.dispatch/rules/pure-core.md b/.dispatch/rules/pure-core.md
new file mode 100644
index 0000000..27edd0e
--- /dev/null
+++ b/.dispatch/rules/pure-core.md
@@ -0,0 +1,6 @@
+# Rule: pure core / imperative shell
+
+Decision logic is pure (input → output), no I/O, no ambient state.
+Effects (fs, db, shell, network, clock, random) are injected at the edges.
+If a function reaches for a global/singleton instead of receiving it, refactor.
+This is for testability, not purity dogma — stop where it would only add ceremony.
diff --git a/.dispatch/rules/typed-handles.md b/.dispatch/rules/typed-handles.md
new file mode 100644
index 0000000..00c539d
--- /dev/null
+++ b/.dispatch/rules/typed-handles.md
@@ -0,0 +1,7 @@
+# Rule: typed handles, no string-keyed coupling
+
+Every cross-extension coupling is anchored to an exported TYPED symbol:
+hook descriptors (`defineHook<T>(...)`) and service handles, never a raw string
+at the consumer site. A string-keyed cross-feature lookup must be a compile
+error, so `lsp references` on the symbol returns the true blast radius.
+Exception: the kernel routing a tool-call by name (that's data, not a code ref).
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..4cc5dca
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+# Local test credentials — copy to .env (gitignored), never commit.
+# OpenCode Go (OpenAI-compatible endpoint).
+DISPATCH_API_KEY=sk-...
+DISPATCH_BASE_URL=https://opencode.ai/zen/go/v1
+DISPATCH_MODEL=deepseek-v4-flash
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fcdac3a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+node_modules/
+dist/
+build/
+*.tsbuildinfo
+
+# Local test credentials — never commit.
+.env
+
+# Agent work reports — local only.
+reports/
+
+# OS / editor noise
+.DS_Store
+*.log
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..174abc7
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,67 @@
+# Dispatch — Constitution (root AGENTS.md)
+
+> Loaded every session. Non-obvious, project-specific rules only. If a fresh
+> frontier model could infer it from the code, it is NOT here (P6).
+> The full design rationale lives in `notes/restructure-plan.md`.
+
+## What this project is
+A **minimal kernel + extensions** agent runtime. The kernel runs ONE agent turn
+and hosts extensions. Every feature is an extension. Tiers: **kernel → core →
+standard**. Backend only for now (no frontend).
+
+## Stack
+Bun + TypeScript (strict, project references via `tsc -b`). Biome for
+lint/format (tabs, double quotes, semicolons, width 100). Vitest for tests.
+SQLite via `bun:sqlite`. AI SDK (`ai` + `@ai-sdk/*`) for providers.
+
+## The non-negotiable architecture rules
+- **Kernel touches NO I/O and names NO concrete feature.** It owns: contracts
+ (the ABI), the extension host, the turn loop (`runTurn`), the event/hook bus.
+ No `node:fs`, `bun:sqlite`, `node:child_process`, or network in the kernel.
+- **Effects live at the edges, injected.** Decision logic is pure
+ (input→output); I/O is passed in. This is for testability, not purity dogma.
+- **No ambient/hidden state.** State is owned and passed explicitly. No stateful
+ singletons. A turn's tool-set must be reproducible from its inputs.
+- **One owner per unit.** Each file/module has exactly ONE agent that edits it.
+ You may ONLY edit the files you are explicitly assigned. To change another
+ unit, you do NOT edit it — you report the needed change up to the orchestrator.
+- **Contracts are the only cross-unit surface.** You see other units' contracts,
+ never their implementation. If you need to read another unit's code to do your
+ job, the contract is incomplete — report that up.
+- **Cross-extension coupling is anchored to exported TYPED symbols** (hook
+ descriptors, service handles). No string-keyed cross-feature lookups — they
+ must be a compile error, so `lsp references` can find every consumer.
+
+## Tool dispatch (kernel turn loop)
+Togglable `{ maxConcurrent, eager }`. `maxConcurrent`: 0=unlimited, 1=sequential,
+2+=cap. `eager`: launch a tool the instant its call streams in vs. after the
+step ends. **Default `{ maxConcurrent: 1, eager: true }`.**
+
+## Durability (never leave the system broken)
+Persist incrementally + append-only. Recovery is a PURE `reconcile(rows)` run on
+load that repairs any partial turn (e.g. a tool-call with no result → synthesize
+an error result). Status is derived/boot-swept, never trusted from disk.
+
+## Testing (asymmetric — strict core, lenient shell)
+- **Pure core / kernel:** zero internal mocks. A test that mocks our own module
+ is a DESIGN BUG — fix the code (inject the effect), not the test. Demand high
+ coverage; it's cheap because the code is pure.
+- **Imperative shell** (orchestrator, transport, real adapters): a few
+ integration tests against real / in-memory backends. Do NOT chase pure-unit
+ coverage here, and do NOT mock sibling extensions.
+- Mocking the OUTERMOST edge (real network/clock) is fine; mocking `@dispatch/*`
+ is a smell.
+
+## Commands
+- `bun run typecheck` — `tsc -b --pretty`
+- `bun run test` — vitest
+- `bun run check` — biome (lint + format)
+
+## Reports
+When you finish a task, write a markdown report to `reports/<your-unit>.md`
+(gitignored): what you built, the public surface, test/typecheck output, and any
+contract gaps or change-requests for other units.
+
+## Vocabulary
+Use the canonical terms in `GLOSSARY.md`. Never invent a synonym for an existing
+concept. Prefer standard/training-baked names.
diff --git a/GLOSSARY.md b/GLOSSARY.md
new file mode 100644
index 0000000..4d70e0e
--- /dev/null
+++ b/GLOSSARY.md
@@ -0,0 +1,27 @@
+# Glossary — canonical vocabulary
+
+> One name per concept. Never invent a synonym. New term? The orchestrator
+> proposes the standard/training-baked name and the user confirms before it lands
+> here. "Aliases to avoid" maps wrong names back to the canonical one.
+
+| Term | Meaning | Aliases to avoid |
+|---|---|---|
+| **kernel** | The minimal runtime core: contracts, extension host, turn loop, event/hook bus. Touches no I/O, names no feature. NOT an extension. | core (when meaning the runtime), engine |
+| **core** (tier) | The extension tier required to complete one turn end-to-end ("minimal Dispatch"). | — |
+| **standard** (tier) | Extension tier shipped on-by-default; the features people think of as Dispatch. | — |
+| **extension** | A unit that contributes capabilities via the Host API: tools, providers, auth, hooks, routes, services, migrations. | plugin, module (when meaning an extension) |
+| **contract** | An extension's typed, exported surface: what it exposes + what it requires. The ONLY thing other units see. | interface (when meaning the whole surface), API |
+| **manifest** | An extension's declaration: id, version, apiVersion, dependsOn, contributions, capabilities, trust. | — |
+| **Host API** | The object an extension receives in `activate(host)`. | host context |
+| **turn** | One user message → assistant response cycle (may span multiple steps). | — |
+| **step** | One LLM round-trip within a turn (may emit multiple tool calls). | iteration |
+| **tool call** | A model's request to run a tool within a step. | function call (when meaning a tool call) |
+| **chunk** | One ordered piece of a message (text, thinking, tool-call/result, etc.), append-only in the log. | block, segment |
+| **runTurn** | The kernel's turn loop: takes provider + messages + tools + dispatch policy, streams, dispatches tools, emits events. | run, agentLoop |
+| **hook** | A typed extension point. **event** = fire-and-forget, N listeners, error-isolated. **filter** = ordered value-in→value-out chain, in-band. | callback (when meaning a hook), listener |
+| **service** | A single-responder request/response capability fetched via a typed handle. NOT a hook. | — |
+| **dispatch policy** | `{ maxConcurrent, eager }` controlling how the turn loop runs a step's tool calls. | — |
+| **reconcile** | The pure function run on load that repairs a partial/interrupted turn into a valid history. | recover, repair |
+| **session-orchestrator** | The core extension that drives a turn: load history → resolve provider/tools → call `runTurn` → persist. | — |
+| **conversation-store** | The core extension persisting the append-only turn/chunk log. | message store |
+| **provider** | An extension wrapping an LLM backend (`stream(messages, tools)`), provider-agnostic to the kernel. | — |
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..07a380a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# Dispatch (architecture rewrite)
+
+Minimal **kernel + extensions** agent runtime. The kernel runs one agent turn and
+hosts extensions; every feature is an extension. Tiers: kernel → core → standard.
+
+- Design & rationale: `notes/restructure-plan.md`
+- Agent constitution: `AGENTS.md`
+- Vocabulary: `GLOSSARY.md`
+
+## Dev
+```
+bun install
+bun run typecheck # tsc -b
+bun run test # vitest
+bun run check # biome
+```
+
+Status: **MVP in progress** — kernel + core extensions to complete one turn
+against OpenCode Go (flash), verified via a thin HTTP `/chat` + curl.
diff --git a/biome.json b/biome.json
new file mode 100644
index 0000000..63b7a19
--- /dev/null
+++ b/biome.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
+ "linter": { "enabled": true, "rules": { "recommended": true } },
+ "formatter": { "enabled": true, "indentStyle": "tab", "lineWidth": 100 },
+ "javascript": { "formatter": { "quoteStyle": "double", "semicolons": "always" } },
+ "files": { "includes": ["**", "!**/node_modules", "!**/dist", "!**/build"] }
+}
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..50b0094
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,250 @@
+{
+ "lockfileVersion": 1,
+ "configVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "dispatch",
+ "devDependencies": {
+ "@biomejs/biome": "^2.4.15",
+ "@types/bun": "^1.3.14",
+ "typescript": "^5.7.0",
+ "vitest": "^3.0.0",
+ },
+ },
+ "packages/kernel": {
+ "name": "@dispatch/kernel",
+ "version": "0.0.0",
+ },
+ },
+ "packages": {
+ "@biomejs/biome": ["@biomejs/[email protected]", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.16", "@biomejs/cli-darwin-x64": "2.4.16", "@biomejs/cli-linux-arm64": "2.4.16", "@biomejs/cli-linux-arm64-musl": "2.4.16", "@biomejs/cli-linux-x64": "2.4.16", "@biomejs/cli-linux-x64-musl": "2.4.16", "@biomejs/cli-win32-arm64": "2.4.16", "@biomejs/cli-win32-x64": "2.4.16" }, "bin": { "biome": "bin/biome" } }, "sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA=="],
+
+ "@biomejs/cli-darwin-arm64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A=="],
+
+ "@biomejs/cli-darwin-x64": ["@biomejs/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw=="],
+
+ "@biomejs/cli-linux-arm64": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ=="],
+
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg=="],
+
+ "@biomejs/cli-linux-x64": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ=="],
+
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg=="],
+
+ "@biomejs/cli-win32-arm64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A=="],
+
+ "@biomejs/cli-win32-x64": ["@biomejs/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw=="],
+
+ "@dispatch/kernel": ["@dispatch/kernel@workspace:packages/kernel"],
+
+ "@esbuild/aix-ppc64": ["@esbuild/[email protected]", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
+
+ "@esbuild/android-arm": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
+
+ "@esbuild/android-arm64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="],
+
+ "@esbuild/android-x64": ["@esbuild/[email protected]", "", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="],
+
+ "@esbuild/darwin-arm64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="],
+
+ "@esbuild/darwin-x64": ["@esbuild/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="],
+
+ "@esbuild/freebsd-arm64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="],
+
+ "@esbuild/freebsd-x64": ["@esbuild/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="],
+
+ "@esbuild/linux-arm": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="],
+
+ "@esbuild/linux-arm64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="],
+
+ "@esbuild/linux-ia32": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="],
+
+ "@esbuild/linux-loong64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="],
+
+ "@esbuild/linux-mips64el": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="],
+
+ "@esbuild/linux-ppc64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="],
+
+ "@esbuild/linux-riscv64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="],
+
+ "@esbuild/linux-s390x": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="],
+
+ "@esbuild/linux-x64": ["@esbuild/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="],
+
+ "@esbuild/netbsd-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="],
+
+ "@esbuild/netbsd-x64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="],
+
+ "@esbuild/openbsd-arm64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="],
+
+ "@esbuild/openbsd-x64": ["@esbuild/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="],
+
+ "@esbuild/openharmony-arm64": ["@esbuild/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="],
+
+ "@esbuild/sunos-x64": ["@esbuild/[email protected]", "", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="],
+
+ "@esbuild/win32-arm64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="],
+
+ "@esbuild/win32-ia32": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="],
+
+ "@esbuild/win32-x64": ["@esbuild/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/[email protected]", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@rollup/rollup-android-arm-eabi": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm" }, "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA=="],
+
+ "@rollup/rollup-android-arm64": ["@rollup/[email protected]", "", { "os": "android", "cpu": "arm64" }, "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw=="],
+
+ "@rollup/rollup-darwin-arm64": ["@rollup/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA=="],
+
+ "@rollup/rollup-darwin-x64": ["@rollup/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ=="],
+
+ "@rollup/rollup-freebsd-arm64": ["@rollup/[email protected]", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw=="],
+
+ "@rollup/rollup-freebsd-x64": ["@rollup/[email protected]", "", { "os": "freebsd", "cpu": "x64" }, "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw=="],
+
+ "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA=="],
+
+ "@rollup/rollup-linux-arm-musleabihf": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm" }, "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ=="],
+
+ "@rollup/rollup-linux-arm64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg=="],
+
+ "@rollup/rollup-linux-arm64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w=="],
+
+ "@rollup/rollup-linux-loong64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ=="],
+
+ "@rollup/rollup-linux-loong64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ=="],
+
+ "@rollup/rollup-linux-ppc64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g=="],
+
+ "@rollup/rollup-linux-ppc64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw=="],
+
+ "@rollup/rollup-linux-riscv64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g=="],
+
+ "@rollup/rollup-linux-riscv64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "none" }, "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ=="],
+
+ "@rollup/rollup-linux-s390x-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g=="],
+
+ "@rollup/rollup-linux-x64-gnu": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q=="],
+
+ "@rollup/rollup-linux-x64-musl": ["@rollup/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw=="],
+
+ "@rollup/rollup-openbsd-x64": ["@rollup/[email protected]", "", { "os": "openbsd", "cpu": "x64" }, "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg=="],
+
+ "@rollup/rollup-openharmony-arm64": ["@rollup/[email protected]", "", { "os": "none", "cpu": "arm64" }, "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA=="],
+
+ "@rollup/rollup-win32-arm64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g=="],
+
+ "@rollup/rollup-win32-ia32-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "ia32" }, "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ=="],
+
+ "@rollup/rollup-win32-x64-gnu": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ=="],
+
+ "@rollup/rollup-win32-x64-msvc": ["@rollup/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw=="],
+
+ "@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
+
+ "@types/chai": ["@types/[email protected]", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/deep-eql": ["@types/[email protected]", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/estree": ["@types/[email protected]", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
+
+ "@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg=="],
+
+ "@vitest/expect": ["@vitest/[email protected]", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.6", "@vitest/utils": "3.2.6", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ=="],
+
+ "@vitest/mocker": ["@vitest/[email protected]", "", { "dependencies": { "@vitest/spy": "3.2.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw=="],
+
+ "@vitest/pretty-format": ["@vitest/[email protected]", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA=="],
+
+ "@vitest/runner": ["@vitest/[email protected]", "", { "dependencies": { "@vitest/utils": "3.2.6", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q=="],
+
+ "@vitest/snapshot": ["@vitest/[email protected]", "", { "dependencies": { "@vitest/pretty-format": "3.2.6", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw=="],
+
+ "@vitest/spy": ["@vitest/[email protected]", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg=="],
+
+ "@vitest/utils": ["@vitest/[email protected]", "", { "dependencies": { "@vitest/pretty-format": "3.2.6", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg=="],
+
+ "assertion-error": ["[email protected]", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
+
+ "cac": ["[email protected]", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
+
+ "chai": ["[email protected]", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
+
+ "check-error": ["[email protected]", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
+
+ "debug": ["[email protected]", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "deep-eql": ["[email protected]", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
+
+ "es-module-lexer": ["[email protected]", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
+
+ "esbuild": ["[email protected]", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
+
+ "estree-walker": ["[email protected]", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "expect-type": ["[email protected]", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "fdir": ["[email protected]", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fsevents": ["[email protected]", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "js-tokens": ["[email protected]", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
+
+ "loupe": ["[email protected]", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="],
+
+ "magic-string": ["[email protected]", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
+
+ "nanoid": ["[email protected]", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
+
+ "pathe": ["[email protected]", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "pathval": ["[email protected]", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
+
+ "picocolors": ["[email protected]", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["[email protected]", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
+ "postcss": ["[email protected]", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A=="],
+
+ "rollup": ["[email protected]", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.61.1", "@rollup/rollup-android-arm64": "4.61.1", "@rollup/rollup-darwin-arm64": "4.61.1", "@rollup/rollup-darwin-x64": "4.61.1", "@rollup/rollup-freebsd-arm64": "4.61.1", "@rollup/rollup-freebsd-x64": "4.61.1", "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", "@rollup/rollup-linux-arm-musleabihf": "4.61.1", "@rollup/rollup-linux-arm64-gnu": "4.61.1", "@rollup/rollup-linux-arm64-musl": "4.61.1", "@rollup/rollup-linux-loong64-gnu": "4.61.1", "@rollup/rollup-linux-loong64-musl": "4.61.1", "@rollup/rollup-linux-ppc64-gnu": "4.61.1", "@rollup/rollup-linux-ppc64-musl": "4.61.1", "@rollup/rollup-linux-riscv64-gnu": "4.61.1", "@rollup/rollup-linux-riscv64-musl": "4.61.1", "@rollup/rollup-linux-s390x-gnu": "4.61.1", "@rollup/rollup-linux-x64-gnu": "4.61.1", "@rollup/rollup-linux-x64-musl": "4.61.1", "@rollup/rollup-openbsd-x64": "4.61.1", "@rollup/rollup-openharmony-arm64": "4.61.1", "@rollup/rollup-win32-arm64-msvc": "4.61.1", "@rollup/rollup-win32-ia32-msvc": "4.61.1", "@rollup/rollup-win32-x64-gnu": "4.61.1", "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA=="],
+
+ "siginfo": ["[email protected]", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
+
+ "source-map-js": ["[email protected]", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "stackback": ["[email protected]", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["[email protected]", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
+
+ "strip-literal": ["[email protected]", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
+
+ "tinybench": ["[email protected]", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["[email protected]", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
+
+ "tinyglobby": ["[email protected]", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
+
+ "tinypool": ["[email protected]", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
+
+ "tinyrainbow": ["[email protected]", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
+
+ "tinyspy": ["[email protected]", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="],
+
+ "typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
+
+ "undici-types": ["[email protected]", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="],
+
+ "vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="],
+
+ "vite-node": ["[email protected]", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
+
+ "vitest": ["[email protected]", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.6", "@vitest/mocker": "3.2.6", "@vitest/pretty-format": "^3.2.6", "@vitest/runner": "3.2.6", "@vitest/snapshot": "3.2.6", "@vitest/spy": "3.2.6", "@vitest/utils": "3.2.6", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.6", "@vitest/ui": "3.2.6", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw=="],
+
+ "why-is-node-running": ["[email protected]", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
+ }
+}
diff --git a/dispatch.toml b/dispatch.toml
new file mode 100644
index 0000000..85eb9f6
--- /dev/null
+++ b/dispatch.toml
@@ -0,0 +1,7 @@
+# Dispatch (arch-rewrite) project config.
+#
+# LSP servers (typescript-language-server, biome) are inherited from the global
+# ~/.config/dispatch/dispatch.toml — no per-repo LSP block needed. Agents can use
+# the `lsp` tool (diagnostics / references / definition) on .ts files directly.
+#
+# Credentials for the rewrite live in .env (gitignored), read by auth-apikey.
diff --git a/notes/restructure-plan.md b/notes/restructure-plan.md
new file mode 100644
index 0000000..1c9e5e6
--- /dev/null
+++ b/notes/restructure-plan.md
@@ -0,0 +1,1451 @@
+# Dispatch Restructure — Living Plan
+
+> **Status:** Planning only. No implementation has begun.
+> **Purpose:** Capture the target architecture, the engineering principles that
+> govern it, and the current-state map — so any agent or human picking this up
+> has the full picture in one place. This is a *living* document: update it as
+> decisions are made and pieces land.
+
+---
+
+## 0. The goal in one paragraph
+
+Restructure Dispatch so the **kernel is the absolute minimum** — just enough to
+run an agent turn and host extensions — and **every feature is an extension**.
+Extensions must be creatable and loadable *from outside this project* (custom /
+third-party extensions), with identical contracts to the bundled ones. For now
+we are planning the **backend only**; the frontend will be reworked separately
+and modularly later, so **no design decision here should be driven by the current
+frontend**.
+
+---
+
+## 1. Engineering principles (the standard for this project)
+
+These are adopted because each solves a **specific, named problem in this
+codebase** — not because they are popular. Each carries its stopping point so we
+don't over-apply it.
+
+### P1 — Feature-as-a-library
+Every feature is independently importable with a clean, documented, minimal API.
+The acceptance test: *can you import just this feature and use it standalone,
+without dragging in the whole app?*
+
+- **Evidence:** `agent-manager.ts` is ~2,453 lines where no single behavior
+ (queueing, tool-assembly, fallback) can be extracted or reasoned about in
+ isolation. By contrast `chunks/transform.ts` is deliberately DB-free so the
+ backend *and* frontend share the same pure logic — feature-as-a-library done
+ right, already in the repo.
+- **Stopping point:** Do **not** over-split into dozens of micro npm packages
+ with version-skew and `package.json` ceremony. Internal import-cleanliness
+ first; a separately *publishable* package only when there's a genuine outside
+ consumer.
+
+### P2 — Functional core / imperative shell
+Pure *decision* logic ("given this state + event, what should happen?") as pure
+functions; the actual I/O (shell, fs, LLM, SQLite) lives in thin adapters
+**injected** at the edges.
+
+- **Evidence:** `wake-scheduler.ts` already does this and says so: "Pure helpers…
+ side-effect-free so the logic can be unit-tested without spinning up Hono or
+ touching SQLite." The giant `vi.mock("@dispatch/core")` blocks in
+ `agent-manager.test.ts` exist *because* effects are reached for instead of
+ passed in.
+- **The honest framing:** An agent system *is* side effects — running shell,
+ writing files, calling the LLM are the product. The goal is **testability and
+ predictability, not purity for its own sake.**
+- **Stopping point:** Where separating decision from effect makes a unit
+ obviously testable, do it. Where it would only add ceremony (DI containers,
+ effect-wrapper types) around an unavoidable `await spawn(cmd)`, don't. Purity
+ is a means; if it stops paying for itself, drop it.
+
+### P3 — No ambient / hidden state
+State is **owned and passed explicitly**, never reached for as a hidden global or
+stateful singleton.
+
+- **Evidence:** Wishlist bugs #16 ("agent tools leak across tabs") and #17
+ ("agent/model setting changes on tab switch") are *caused by* shared mutable
+ singletons / frontend-held state. Explicit per-tab state ownership fixes them
+ structurally.
+- **Stopping point:** Stateless classes-as-namespaces are fine. Stateful
+ god-objects (today's managers) are the thing we're killing. The tool-set for a
+ turn must be reproducible from `(agent profile + capabilities + active
+ extensions)` — pure input → output.
+
+### P4 — Don't adopt by reputation (meta-principle)
+Every pattern, library, or methodology — **including the "minimal kernel +
+extensions" architecture itself** — earns its place by solving a specific,
+named problem in *this* codebase, and we note where it stops paying off. "It's a
+known good practice" is a hypothesis to test, not a justification.
+
+### P5 — The repo is a harness, not just code
+Meta-information that guides future agents is a **first-class deliverable**,
+maintained like code. Modeled as a *tiered cache* of context: small
+always-loaded files + larger on-demand files, so an agent gets the right info at
+the right moment without burning context.
+(Source: "The AI Harness" — see §7. Bounded to our scale in §7.4.)
+
+### P6 — Document only the non-inferable
+Harness docs contain **tribal knowledge and scar tissue only** — never generic
+best-practice the model already knows. Test: *"Could a fresh frontier model
+figure this out by reading the code? If yes, leave it out."*
+(This is P4 applied to documentation — it self-limits harness bloat.)
+
+### P7 — The harness is extension-scoped
+Every extension ships **its own** constitution snippet, safety rules, feature
+doc, glossary terms, and skills — portable with the code. This is P1
+(feature-as-a-library) applied to documentation: import the extension, get its
+harness too. Better than a repo-global harness for a modular system.
+
+### P8 — One canonical vocabulary
+A `GLOSSARY.md` with an **"aliases to avoid"** column governs naming. New code
+reuses existing terms; it never invents a synonym for an existing concept.
+
+- **Evidence:** This codebase overloads **tab / session / conversation** and
+ **chunk / message / turn / step** — the chunk-log refactor notes exist
+ precisely because those terms got tangled.
+- **Live application:** "core" now has a precise meaning (the extension tier in
+ §2.6) — it must NOT be reused for the kernel. Kernel ≠ core.
+
+---
+
+## 2. Target architecture — minimal kernel + extensions
+
+### 2.1 Layered picture
+
+```
+┌───────────────────────────────────────────────────────────────┐
+│ Clients (any frontend — reworked later, out of scope now) │
+└───────────────────────────────────────────────────────────────┘
+ ▲ typed events / commands (via a transport extension)
+┌───────────────────────────────────────────────────────────────┐
+│ STANDARD extensions (the features people think of as Dispatch)│
+│ tools (read_file, run_shell…) · agents · skills · lsp · │
+│ compaction · notifications · scheduler · attachments · … │
+└───────────────────────────────────────────────────────────────┘
+ ▲ depend on kernel + core (never upward)
+┌───────────────────────────────────────────────────────────────┐
+│ CORE extensions (minimum glue to run ONE turn end-to-end) │
+│ transport · provider · auth · session-orchestrator │
+└───────────────────────────────────────────────────────────────┘
+ ▲ register contributions ▲ receive Host API
+┌───────────────────────────────────────────────────────────────┐
+│ KERNEL (minimal; not an extension) │
+│ │
+│ Extension Host Agent Runtime Event/Hook Bus │
+│ (discover/resolve/ (the turn loop, (typed pub/sub │
+│ activate/registries) provider+tool + filters) │
+│ agnostic) │
+│ │
+│ Kernel Services (exposed through Host API): │
+│ • Capability/Permission gate • Config (merge + schema) │
+│ • Storage + migration runner • Secret/credential vault │
+│ • Conversation/chunk store • Logger │
+│ │
+│ Contracts (the stable ABI every extension compiles against) │
+└───────────────────────────────────────────────────────────────┘
+```
+
+### 2.2 The Kernel — the "absolute minimum"
+
+Five things, nothing more:
+
+1. **Contracts (the stable ABI).** The only types extensions depend on, versioned
+ independently from implementations. Seeded from today's `types/index.ts`:
+ - `ToolContract` (today's `ToolDefinition`: `{ name, description, parameters,
+ execute(args, ctx) }`) — see §3.3 for the `ctx` requirements concurrency forces.
+ - `ProviderContract` (model factory + streaming + catalog/capability entries)
+ - `AuthContract` (credential sources / OAuth flows feeding the vault)
+ - `Extension` + `Manifest` (id, version, apiVersion range, deps, activation,
+ contributions, capabilities)
+ - `HostAPI` (what an extension receives on activate — see §2.3)
+ - `Hook`/event taxonomy (the lifecycle surface)
+ - Conversation model (`ChatMessage`, `Chunk`, turn/step)
+
+2. **Extension Host.** Discover → validate manifest → resolve dependency DAG →
+ check apiVersion compat → run migrations → activate (topological) → register
+ contributions → dispose on shutdown/reload. Owns the **registries** (tools,
+ providers, hooks, routes/commands, services, settings, migrations, jobs).
+
+3. **Agent Runtime (the turn loop).** The refactored heart of today's `agent.ts`:
+ takes *a resolved provider + a tool set + messages + a dispatch policy*,
+ streams, dispatches tool calls (see §3.3), dedups, truncates/spills, emits
+ events. **Provider-agnostic and tool-agnostic** — knows only the contracts.
+ Names no concrete tool or provider.
+
+4. **Event / Hook Bus.** Typed pub/sub plus *filters*:
+ - **Observers** react (notifications, persistence, usage accounting).
+ - **Filters** transform in a chain (system-prompt assembly, message pre-send,
+ tool-result transform, tool-set filtering).
+
+5. **Kernel Services (via Host API).** The kernel exposes *interfaces* and pure
+ logic here — **never concrete I/O backends** (those are `core` extensions; see
+ §2.8). This keeps "kernel touches no I/O" (§2.7) literally true.
+ - **Config loader** — merged loader (global → project) + per-extension
+ settings schema/validation. **Must be in the kernel** (not an extension):
+ it's needed at boot to *find and resolve* extensions — a chicken-and-egg the
+ extension system itself can't solve. Seeded from today's `config/`.
+ - **Logger** — always-on, available before any extension activates.
+ - **Permission rule *evaluation*** — the pure `evaluate(rules, request) →
+ decision` function (today's `permission/evaluate.ts`): rules in, decision
+ out, no I/O. The *interactive prompting* (asking a human, today's
+ `permission-manager.ts`) is a transport/UI concern owned by a `core`/
+ `standard` extension, not the kernel.
+ - **Storage interface + migration runner** — the kernel defines the storage
+ *contract* (namespaced KV/SQL + per-extension migration registration) and
+ exposes `host.storage(ns)`, but the **concrete backend (SQLite) is a `core`
+ extension** (`storage-sqlite`), swappable for an in-memory store in tests
+ (serves P2 directly). Bootstrap ordering: the storage backend activates
+ first (no deps) so later extensions can run their migrations.
+ - **Secret/credential vault interface** — `host.secrets` (capability-gated);
+ the concrete store and the *auth flows* that fill it are extensions.
+ - **Conversation/chunk store** — NOTE: the kernel owns only the **conversation
+ model TYPES** (`Chunk`/`ChatMessage` in contracts) and the pure
+ explode/group transforms (today's DB-free `chunks/transform.ts`). The
+ **persistent store itself is a `core` extension** built on `host.storage` —
+ because persistence is I/O. The runtime reads/writes history *through the
+ orchestrator*, which calls the store; the kernel's `runTurn` takes
+ `messages` as a plain input and returns result messages (it never touches
+ the DB).
+
+> **Deliberately NOT in the kernel:** any concrete tool, any provider, any
+> concrete persistence/secret backend, the persona/system-prompt text, the HTTP
+> server, interactive permission prompting, tab/queue orchestration, sub-agents,
+> skills, LSP, notifications, compaction, scheduling.
+
+### 2.3 The extension model
+
+- **What it is:** a directory or npm package with a **manifest** + entry module
+ exporting `activate(host)` and optional `deactivate()`.
+- **Manifest shape:** `id, name, version, apiVersion (semver range), dependsOn[],
+ activation ("eager" | lazy event triggers), contributes {tools, providers,
+ routes, commands, hooks, settings, migrations, scheduledJobs, services},
+ capabilities {fs, shell, network, secrets, db, spawn…}, settingsSchema`.
+- **Each extension's contract is two-sided (provides + expects):** what it
+ *exposes* (its contributions/services) and what it *expects exposed to it*
+ (its `dependsOn` services + `capabilities`). This two-sided contract is what
+ the host uses to resolve load order and what makes an extension portable.
+- **Host API (what `activate(host)` receives):**
+ - `host.defineTool/defineProvider/defineAuth(...)`
+ - `host.defineRoute/defineCommand(...)` — for transports & UI actions
+ - `host.on(hook, handler)` / `host.addFilter(hook, fn)`
+ - `host.provideService(handle, impl)` / `host.getService(handle)` — typed DI
+ via **typed service handles** (an exported symbol, NOT a raw string — so
+ `lsp references` can compute a service's consumers; see §5)
+ - `host.storage(namespace)` — scoped KV/SQL + migrations (interface; backed
+ by the `storage-sqlite` core extension — see §2.8)
+ - `host.config` / `host.settings`
+ - `host.secrets` (capability-gated)
+ - `host.permissions.check(request)`
+ - `host.events.emit(...)` / `host.logger`
+ - `host.scheduler.register(job)`
+- **Contribution points** (replacing today's wiring):
+ | Point | Replaces today's | Examples |
+ |---|---|---|
+ | tools | per-turn assembly in `agent-manager` | read_file, run_shell, web_search |
+ | providers | `llm/provider.ts`, `models/registry` | anthropic, opencode, google |
+ | auth | `credentials/*` | claude OAuth, api-keys |
+ | context filters | `buildSystemPrompt`, skills/agents injection | persona, skills, agent profiles |
+ | hooks/observers | scattered wiring | notifications, usage accounting |
+ | routes/commands | `api/routes/*` | `/chat`, `/tabs`, `/models` |
+ | scheduled jobs | `wake-scheduler.ts` | cache-warm, wake probes |
+ | migrations | `db/index.ts` table block | each extension owns its tables |
+ | services | implicit singletons | LSP manager, model registry |
+- **Loading / lifecycle:** search paths (precedence high→low) =
+ project `.dispatch/extensions` → global `~/.config/dispatch/extensions` →
+ installed npm packages (naming convention) → bundled first-party. Resolve DAG →
+ verify apiVersion → run migrations → activate topologically (lazy ones defer to
+ their activation event) → ready. Hot-reload via watchers (config already does
+ this); deactivate disposes everything the extension registered.
+
+### 2.4 Extension catalog (current code → extensions, with tier)
+
+- **core tier (the minimum to complete one turn — see §2.8):**
+ `storage-sqlite` (concrete backend behind `host.storage`), `conversation-store`
+ (append-only turn/chunk persistence on top of `host.storage`; today's
+ `db/chunks.ts` + `db/tabs.ts`), `transport` (accept message, stream events —
+ HTTP/WS, or even stdio), `provider-×1` (one LLM provider), `auth-×1` (that
+ provider's credentials), `session-orchestrator` (the turn-driver carved out of
+ `agent-manager.ts`).
+- **standard tier — tools:** `tools-fs` (read_file, read_file_slice, write_file,
+ list_files), `tool-shell` (run_shell + background store + shell-analyze),
+ `tool-search` (search_code), `tool-web`, `tool-youtube`, `tool-todo`,
+ `tool-key-usage`.
+- **standard tier — providers & auth beyond the minimum:** `provider-anthropic`,
+ `provider-opencode`, `provider-google`, `provider-copilot`; `auth-claude`
+ (OAuth), `auth-apikeys`, `models-catalog` (registry + capabilities). *(Note:
+ the single provider/auth required to boot is "core"; additional ones are
+ "standard". Which specific one is the core default is a §8 decision.)*
+- **standard tier — subsystems:** `lsp` (manager service + `lsp` tool +
+ diagnostics-on-write filter), `agents` (sub/user-agent system + `summon`/
+ `retrieve`), `skills` (loader + context-filter), `session-features` (tabs,
+ queue, deliverMessage, auto-wake budget, `send_to_tab`/`read_tab` — the parts
+ beyond the minimal orchestrator), `compaction`, `notifications-ntfy`,
+ `wake-scheduler`, `attachments` (multimodal validation/limits).
+
+> Result: **`agent-manager.ts` dissolves** into the kernel's turn loop + the
+> core `session-orchestrator` + standard-tier contributions.
+
+### 2.5 Proposed package layout
+
+```
+packages/
+ kernel/ # the kernel ONLY (NOT named "core" — see P8 / §2.6)
+ contracts/ # the KERNEL ABI ONLY (turn loop, HostAPI, hook/event
+ # mechanism, conversation model) — versioned.
+ # Per-extension contracts are NOT here — they live
+ # co-located in each extension package (see §5).
+ host/ # discovery/resolve/activate + registries
+ runtime/ # the agent turn loop (incl. tool dispatch, §3.3)
+ bus/ # events + filters
+ services/ # config loader, logger, permission eval, storage IFACE + migration runner, secrets IFACE
+ extensions/
+ core/ # core-tier: storage-sqlite, conversation-store, transport,
+ # provider-×1, auth-×1, session-orchestrator
+ standard/ # standard-tier: tools, agents, skills, lsp, compaction, …
+ # each extension package owns its OWN contract
+ # (what it exposes/requires + its hook & service
+ # handles) co-located inside it — see §5
+ host-bin/ # thin bootstrapper: make kernel, point at ext dirs, activate
+ sdk/ # helper toolkit + types for THIRD-PARTY ext authors
+ frontend/ # reworked later
+```
+
+### 2.6 Tiers: kernel → core → standard
+
+We classify extensions into tiers. **Tiers are labels over the dependency DAG,
+not a second enforcement mechanism** — the host resolves load order from each
+extension's declared deps, and the capability gate enforces access. Tiers
+describe *what ships in which distribution*.
+
+| Tier | Objective test | Distribution |
+|---|---|---|
+| **kernel** | the ABI + turn loop; *not* an extension | always |
+| **core** | required to complete one turn end-to-end | "minimal Dispatch" |
+| **standard** | ships on by default; defines Dispatch-as-known | "default Dispatch" |
+| *(external)* | not in this repo | community / custom |
+
+- **No "extras" tier yet.** Empty categories are over-planning. A fourth tier
+ (bundled-but-off-by-default) earns existence only when a real feature is
+ genuinely opt-in — not by demoting an existing feature to fill a slot.
+- **The one invariant that gives tiers teeth — no upward dependencies.** A `core`
+ extension may depend on the kernel and other `core` extensions, never on
+ `standard`. Checkable straight from manifests (a lint). This is what makes
+ "the minimal distribution still boots" *true* rather than aspirational.
+- **Naming (P8):** "core" is the extension tier; the runtime primitive is the
+ **kernel**. Never reuse "core" for the kernel.
+
+**Placement test in action — `read_file` is `standard`, not `core`.** Apply the
+test: remove `read_file` → the agent just replies with text; the turn still
+completes. So it fails the core test → it's `standard`. The surprise that
+validates the model: **tools are not the minimum.** A turn can happen with zero
+tools. `read_file` being *important* is why it ships on-by-default in `standard`
+— not why it's `core` (resisting "important ⇒ core" keeps `core` from regrowing
+into a god-object; P4).
+
+### 2.7 Kernel vs core boundary + how a tool plugs in
+
+**Boundary rule (one sentence):**
+> **Kernel = the pure turn mechanism** (decides nothing, touches no I/O, names no
+> feature). **Core = the minimum glue** that wires real inputs into that
+> mechanism and handles the results — opinionated and effectful, which is exactly
+> why it can't live in the kernel.
+
+**Example — the `session-orchestrator` (core), carved out of `agent-manager.ts`:**
+```ts
+host.on("message.received", async (msg) => {
+ const conversation = await host.conversation.load(msg.tabId); // effect: read state
+ const provider = host.providers.resolve(msg.model); // decision: pick LLM
+ const tools = host.tools.resolveFor(msg.tabId); // decision: gather/filter
+ const dispatch = resolveDispatchPolicy(msg); // decision: §3.3 toggle
+
+ const result = await kernel.runTurn({ // ← call the kernel
+ provider, messages: conversation.messages, tools, dispatch,
+ emit: host.events.emit,
+ });
+
+ await host.conversation.append(msg.tabId, result.messages); // effect: persist
+});
+```
+Every line is a **decision** (which provider/tools/policy) or an **effect**
+(load/persist) — neither belongs in the kernel.
+
+**How a tool builds "on top of" the kernel (inversion of control).** The kernel
+never *finds* tools; it *receives* them. The dependency arrow points
+tool → contract → kernel, never the reverse:
+1. A tool conforms to `ToolContract` (owned by the kernel) — importing only the
+ contract, not the kernel internals or other tools.
+2. It registers at activation: `host.defineTool(createReadFileTool(workdir))`.
+3. The orchestrator gathers them: `host.tools.resolveFor(tabId)`.
+4. They're handed into `runTurn`, which calls them blindly by shape
+ (`byName.get(call.name).execute(...)`). The kernel never knows `read_file`
+ exists. 0, 1, or 50 tools — the loop is identical.
+
+### 2.8 The Minimum Viable Turn (what "core" must contain)
+
+Derived by tracing the **real** end-to-end path of a single message in today's
+code — `POST /chat` → `deliverMessage` → `processMessage` → `getOrCreateAgentForTab`
+(`new Agent`) → `for await (event of agent.run())` → `emit(event)` → `/ws`
+fan-out — and stripping everything not load-bearing.
+
+**Two readings of "send a message, get a response":**
+- **(A) Absolute minimum mechanism** — one stateless request→response; needs *no
+ DB at all*. (Useful as the testing/embedded floor.)
+- **(B) Minimum useful chat** — real multi-turn, so turn 2 sees turn 1. Adds
+ conversation persistence.
+
+**DECIDED: `core` targets (B).** "Minimal Dispatch" is a usable multi-turn chat.
+The single piece separating (B) from (A) is the **conversation store + storage
+backend** — drop those two and you have the stateless (A) floor (which is exactly
+the in-memory test configuration).
+
+**Stripped from the real path → all of these are `standard`, NOT core** (each
+confirmed removable without breaking a basic turn): key/model **fallback chain**
+(`buildFallbackSequence`, rate-limit retry), **tools** entirely (empty tool list
+→ turn still completes as text), **interactive permission prompting** (only
+exercised *by* tools), **reasoningEffort / attachments / workingDirectory**
+overrides, **skills, agents/summon, lsp, notifications, compaction, queue /
+auto-wake, usage telemetry, prompt-cache warming**, and the system-prompt
+**TOOL_DESCRIPTIONS + task-management** assembly (minimal = a plain/empty system
+string). This concretely confirms §2.6's surprise: **tools, persona, and
+permissions are all riders — the turn loop needs none of them.**
+
+**KERNEL exposes (for the minimal turn):**
+| Thing | Why kernel | From today |
+|---|---|---|
+| Contracts (ABI): `ProviderContract`, `ToolContract`, `AuthContract`, `Extension`/`Manifest`, `HostAPI`, event taxonomy, conversation model (`Chunk`/`ChatMessage`) | shared types everything compiles against | `types/index.ts` |
+| Extension Host + registries | nothing runs without discover/resolve/activate | (new) |
+| `runTurn({ provider, messages, tools, dispatch, emit, signal })` | the pure turn loop (§3.3); takes `messages` as input, returns result messages, touches no DB | `agent.ts` |
+| Event bus | how the turn talks to the outside | `onEvent`/`emit` |
+| Config loader | needed at boot to find extensions (chicken-and-egg) | `config/` |
+| Logger | always-on, pre-extension | — |
+| Permission rule *evaluation* (pure) | rules in → decision out | `permission/evaluate.ts` |
+| `host.storage` / `host.secrets` *interfaces* | exposes the shape; backend injected | — |
+
+**CORE provides (the minimum extensions to complete one turn):**
+| Extension | Job on the minimal path |
+|---|---|
+| `storage-sqlite` | concrete backend behind `host.storage` (the (A)↔(B) piece; swap for in-memory in tests) |
+| `conversation-store` | append-only turn/chunk persistence on `host.storage` (so turn 2 sees turn 1) |
+| `transport` | accept the message; stream events back (HTTP/WS, or stdio) |
+| `provider-×1` | call an LLM and stream tokens |
+| `auth-×1` | supply that provider's credentials |
+| `session-orchestrator` | wire it together (below) |
+
+**The minimal turn, end to end (target):**
+```
+transport.receive(msg)
+ → orchestrator: history = conversationStore.load(convId) // core (skip → (A) stateless)
+ → orchestrator: provider = providers.resolve(model) // core ext + auth
+ → kernel.runTurn({ provider, messages: [...history, msg], tools: [], dispatch, emit })
+ → emit(events) → transport.stream(events) // core ext
+ → orchestrator: conversationStore.append(convId, result) // core ext
+```
+Note `tools: []` — a turn completes with zero tools (text reply). Every capability
+beyond this is a `standard` extension that contributes tools / filters / hooks.
+
+### 2.9 Contract versioning (convention now, machinery deferred)
+
+**Reframe first (P4):** semver's machinery exists to coordinate **independent
+release timelines** (a producer ships v2; consumers upgrade whenever). That
+*temporal decoupling* is the problem it solves — and we mostly don't have it:
+- **Internal extensions** (bundled, in-repo): no decoupling. A contract change is
+ found via `lsp references` (§5.3) and fixed atomically in one change set. **The
+ type system IS the version check** — a breaking change is a compile error.
+- **External/custom extensions** (out-of-repo): decoupling is real — the compiler
+ can't see their code. A declared version compatibility gate earns its place
+ **only here.** *(And we don't support external extensions yet — see below.)*
+
+So versioning is **asymmetric**, like §3.6 / §3.7: *internal = the type system is
+the version; external = a declared version is the contract.*
+
+**Two different "versionings" — keep them separate:**
+- **Data/schema migration** (persisted-data evolution) — already decided (§2.2:
+ each extension owns its migrations). NOT this section.
+- **Contract/API-surface versioning** — this section. Independent: a contract can
+ change with no migration, and vice-versa.
+
+**DECISION — convention-only and dormant in 0.x.** Because everything is
+**developed in-house today** (no external extensions), we adopt the *vocabulary*
+of versioning, not the *bureaucracy*:
+- **Every package self-versions.** No enforced lockstep / single repo version:
+ the kernel bumps when the ABI changes; an extension bumps when *its* contract
+ changes. Independent versioning matches one-agent-per-unit (§5) — each owner
+ manages its own.
+- **Semver *meaning* as disciplined changelog hygiene** (and the §5.3 fan-out
+ signal), using the standard terms:
+ - **major** — removing or modifying the contract surface (incl. a hook/service
+ payload shape change). *Breaking.* This bump is the orchestrator's cue to fan
+ out to **all** consumers (found via `lsp references`).
+ - **minor** — adding to the contract surface. Existing consumers unaffected.
+ - **patch** — internal change only; no surface/payload change.
+- **Right now the version is COMMUNICATION, not ENFORCEMENT.** With no external
+ consumers, the type system + `lsp references` are the actual mechanism; the
+ number is a changelog/fan-out signal for humans and agents — not load-bearing.
+- **Stay in `0.x`** (conventionally: "no stability promised") through the rewrite,
+ while the ABI churns. `1.0.0` is reserved for "stable enough to invite external
+ extensions" — and **that** decision is the trigger to build the deferred
+ machinery below. We worry about it when we get there, not before.
+
+**Deliberately NOT built now (deferred until external extensions exist):**
+- A load-time **version-compat gate** (external manifest pins an `apiVersion`
+ range; host disables+surfaces on mismatch per §3.7 fault containment).
+- A mechanical **`.d.ts`-surface-diff** in CI to flag breaking changes
+ automatically (removes semver's human-judgment weakness).
+
+**Harness rule this generates (scoped to contract-defining agents only; written
+into agent files when those agents exist, not now):** "Follow semver on your
+contract: **major** = removed/renamed/retyped export or changed hook/service
+payload (and signals the orchestrator to fan out to all `lsp references`
+consumers); **minor** = additive; **patch** = no surface change. Internal
+consumers are caught by the compiler — the version is for the fan-out signal (and,
+later, external consumers)." *(The term "patch" is training-standard vocabulary,
+so it needs no glossary entry — P6.)*
+
+---
+
+
+### 2.10 Core-default provider/auth (the boot minimum + primary testbench)
+
+**Criterion (not "best provider" — leanest, most-testable core per §2.8/§3.6):**
+the one provider+auth that makes "minimal Dispatch" boot with the smallest auth
+surface and the lightest test setup.
+
+**DECISION: OpenAI-compatible provider + API-key auth is the core default** —
+`provider-openai-compat` + `auth-apikey`. This is *also* the primary testbench:
+**OpenCode Go (flash) IS this path.**
+- In today's code it is `createProvider`'s **default branch**
+ (`createOpenAICompatible`, name `"opencode-zen"`) with the hardcoded defaults
+ `model: "deepseek-v4-flash"`, `baseURL: "https://opencode.ai/zen/go/v1"`, and a
+ plain **API key** — the simplest possible `AuthContract`.
+- **Why it's the right core default (grounded, P4):**
+ 1. **Simplest auth = leanest core.** `apiKey` + `baseURL`, nothing else. Claude
+ OAuth (token refresh, billing/beta headers, session id, account discovery)
+ would bloat the *minimum* tier and contradict §2.8.
+ 2. **Most generic contract shape.** OpenAI-compatible is a near-universal wire
+ format (dozens of providers + local Ollama/LM Studio), so the core's one
+ provider is really "the protocol most of the world implements."
+ 3. **Already the literal default** in `createProvider` — core encodes a decision
+ the codebase already made.
+ 4. **Best for §3.6 testability.** API-key auth fakes trivially (a string + a
+ base URL at a mock server); OAuth would force token-refresh mocking — the
+ exact mock-sprawl we're fighting.
+- **Project fit (the deciding constraint):** the two available subscriptions are
+ **Claude** and **OpenCode Go**. OpenCode Go has the most generous limits/API
+ (especially the **flash** agents) → it is the **primary test bench**. The lean
+ core default and the testbench are therefore the *same* path — no tension.
+
+**Tier placement that follows:**
+- **core:** `provider-openai-compat` + `auth-apikey` (boots minimal Dispatch; =
+ OpenCode Go flash via `/zen/go/v1`).
+- **standard:** `provider-anthropic` + `auth-claude` (OAuth — your daily driver,
+ rides on top), plus the **Anthropic-format OpenCode Go models** (MiniMax/Qwen
+ via `isOpencodeGoAnthropicModel`, a different endpoint than flash),
+ `provider-google`, `provider-copilot`, etc.
+- Mirrors every prior decision: the rich/preferred providers ride on top as
+ standard extensions; core proves the architecture with the simplest path.
+
+**Naming (P8):** `provider-openai-compat`, `auth-apikey` — descriptive,
+training-adjacent; no glossary entry needed.
+
+---
+
+## 3. Runtime flow
+
+### 3.1 Boot
+1. Host process starts kernel with config + extension search paths.
+2. Kernel opens DB, loads merged config, builds the capability gate.
+3. Extension host discovers manifests → resolves DAG → checks apiVersion → runs
+ migrations.
+4. Activates extensions topologically; each registers tools / providers / hooks /
+ routes / services / jobs.
+5. `transport-http` listens; `session-orchestrator` subscribes to message intake;
+ scheduler arms jobs. Ready.
+
+### 3.2 A turn
+1. Inbound message hits a `transport` route → emits `message.received`.
+2. `session-orchestrator` resolves conversation, working dir, the
+ **provider+model+key** (provider registry + auth vault), the agent profile,
+ and the **tool-dispatch policy** (§3.3).
+3. **Context-assembly filter chain** runs: persona + skills + agent profile
+ contribute system prompt and a tool-name filter.
+4. Tool set = tool registry filtered by the **capability gate** + agent whitelist.
+5. **Agent runtime loop:** `provider.stream(messages, tools)` → dispatch tool
+ calls per the policy (§3.3) → gate check → `tool.before` filter → execute
+ (exec context: shell-output streaming, cancellation, queued-message
+ injection) → `tool.after` filter → feed results back; repeat until done.
+6. Events stream on the bus → transport pushes to clients; `notifications`
+ reacts; conversation store appends chunks; usage recorded.
+7. `turn.sealed` hook → `compaction` may trigger; scheduler may schedule
+ cache-warm.
+
+### 3.3 Kernel internals — tool dispatch (togglable: `maxConcurrent` + `eager`)
+
+**Mechanism.** The model streams tool calls *incrementally*: each `tool-call`
+event is fully formed (parsed `input`) **before** the step's `finish-step`. So
+the kernel can launch a call the moment it arrives. Tool calls batched in one
+step are **independent by construction** — the model sees no result until the
+next step — so running them concurrently/eagerly is *semantically safe*, not a
+reordering risk.
+
+**Today (for contrast):** `agent.ts` collects all `tool-call`s during the stream,
+then executes them **after** the loop, **sequentially** (`for … await execute`).
+That is `{ maxConcurrent: 1, eager: false }` — the safe baseline we keep available.
+
+**Two orthogonal axes — the toggle.** A single enum conflated two independent
+questions; we split them so every combination is coherent (no invalid states):
+- `maxConcurrent` (a number) = *how many tools run at once*: `0` → unlimited,
+ `1` → sequential (a concurrency limit of 1 is exactly serial), `2+` → that cap.
+- `eager` (a boolean) = *when execution starts*: `true` → launch each call the
+ instant its `tool-call` streams in (overlaps with the rest of generation);
+ `false` → wait until the step's `finish-step`, then dispatch the batch.
+
+| `maxConcurrent` | `eager` | Meaning |
+|---|---|---|
+| 1 | false | One at a time, after the stream ends → **previous (pre-rework) behavior** |
+| 1 | true | **DEFAULT.** Start the first tool the instant it arrives (overlap with generation), but never run two tools at once — safe for any tool |
+| 2+ | false | Up to N in parallel, after the stream ends |
+| 2+ | true | Up to N in parallel, launched as they stream in |
+| 0 | false | All in parallel, after the stream ends |
+| 0 | true | All in parallel, launched as they stream in |
+
+**The policy is a KERNEL INPUT, never ambient (P3):**
+```ts
+interface ToolDispatchPolicy {
+ maxConcurrent: number; // 0 = unlimited, 1 = sequential, 2+ = cap
+ eager: boolean; // true = launch on arrival; false = after finish-step
+}
+runTurn({ provider, messages, tools, dispatch /* : ToolDispatchPolicy */, emit })
+```
+The kernel receives a *resolved* policy; it never reads config itself.
+
+**`eager` + a limit — exact semantics.** A streaming semaphore: launch on arrival
+until `maxConcurrent` is reached, then queue; as each tool finishes, the next
+(queued or newly-arrived) call starts. Well-defined for every combination above.
+
+**Resolution (who sets it)** — mirrors the existing `reasoningEffort` precedence:
+per-turn/tab override → agent definition → global config (`dispatch.toml`) →
+built-in default. The `session-orchestrator` (core) resolves this and hands the
+final value to the kernel.
+
+**Default — DECIDED: `{ maxConcurrent: 1, eager: true }`.** Never two tools at
+once (safe for any tool, incl. non-concurrency-safe ones), yet still overlaps the
+first tool's execution with the rest of generation — zero risk, free latency.
+Raising `maxConcurrent` (e.g. 4) is the opt-in throughput win; `0` (unlimited) is
+a deliberate, footgun-aware opt-in (see complication #2).
+
+**Contract requirements this forces (must be in `ToolContract`/`ctx` on day one
+— retrofitting later is painful):**
+- `ctx.onOutput(data, stream)` — streaming output the **kernel attributes by
+ `toolCallId`**, so concurrent shell output doesn't interleave ambiguously
+ (today's `shell-output` event carries no id — fine only because exec is
+ sequential).
+- `ctx.signal` — cancellation, so an aborted turn doesn't leak in-flight tool
+ work.
+- **`execute` must be safe to run concurrently** with other tools (no shared
+ ambient state — this is just P3 paying off).
+
+**Optional refinement (note, don't build yet):** a tool may declare
+`concurrencySafe: false` in its contract; the kernel serializes *those* even when
+`maxConcurrent` allows parallelism — so one mutating tool doesn't force the whole
+batch sequential. This overrides the global setting **downward only** (never
+widens parallelism).
+
+**Complications checklist (carried from today's sequential code):**
+1. **Shell-output attribution** → tag by `toolCallId` (above).
+2. **Concurrency cap + dedup** → bound parallelism; populate the byte-identical-
+ call dedup map in emission order (the "150 identical calls" incident — do not
+ fire 150 effects at once). `maxConcurrent: 0` (unlimited) re-opens this
+ footgun for *distinct* calls, so it must stay a deliberate opt-in, never the
+ default.
+3. **User-interrupt injection** → target the last call by **batch index**, not
+ completion time (results return nondeterministically under concurrency).
+4. **Abort / error cleanup** → await or cancel in-flight tools via `ctx.signal`;
+ synthesize residual results for orphaned tool-call IDs (today's safety nets).
+5. **Wasted effects on abort** → eager exec may complete a side-effecting tool
+ (`run_shell`) *before* an abort; the effect already happened, result
+ discarded. Accepted consciously for non-idempotent tools.
+
+**Scope boundary.** This is **within a step's batch only**. Next-step tools can't
+start early — they don't exist until the model sees this step's results. So
+"before the turn ends" = "across the multiple tool calls in one step," which is
+exactly the multi-tool-call case.
+
+### 3.4 State, durability & crash recovery
+
+**The worry (context):** a chat must survive *any* interruption — random shutdown,
+token exhaustion, tool error — and the user just resumes with the same history,
+never facing a "wipe it clean and start over" broken state.
+
+**What today's code already gets right (keep this):**
+- `appendChunks` wraps a whole turn's rows in **one SQLite transaction** + WAL →
+ **atomic**: a hard crash mid-write yields *all* those rows or *none*. No half
+ rows, no DB corruption. This is the most important property and it already holds.
+- History is an **append-only chunk log** keyed by monotonic per-tab `seq`. Prior
+ history is never mutated, so a crash can't corrupt what's already written.
+
+**The real danger window (what to fix):** the whole assistant turn is accumulated
+**in memory** (`chunks: Chunk[]`) and written **once at the end** (`flushAssistant`
+on seal). A mid-turn crash loses the *entire* assistant turn. Two latent issues
+compound it:
+1. **Orphaned `running` status** — `status` is persisted to `tabs`; a crash leaves
+ it `running` forever (no boot reconciliation resets stale `running → idle`).
+2. **Orphaned tool-call IDs** — a crash between an assistant `tool_call` and its
+ `tool_result` leaves a dangling call. Anthropic **rejects** such a history
+ (`MissingToolResultsError`). Today's `synthesizeResidualToolResults` guards
+ this *in memory* only — useless once the process is dead. **This is the exact
+ "history the provider refuses to accept → start over" failure.**
+
+```
+user message ──► [persisted immediately ✓]
+ │
+ ├─ assistant streams text/thinking/tool-calls ──► accumulates IN MEMORY ONLY
+ │ (50 steps, tool runs, minutes…)
+ │ ◄── CRASH HERE ──► entire assistant turn GONE; maybe a dangling tool_call
+ │
+ └─ turn seals ──► flushAssistant() ──► [persisted ✓]
+```
+
+**The design — make broken state *unreachable*, not just recoverable.** Four
+rules, each tied to a real failure above:
+
+- **R1 — Persist incrementally, append-only (kill the in-memory window).** Write
+ each step (not each delta) to the log as it completes, in its own transaction.
+ A crash then loses at most the *last in-flight step*, not the whole turn.
+ Granularity = per **step**, not per **delta** (a handful of writes per turn, not
+ hundreds) — keeps IO modest. Make granularity configurable.
+- **R2 — Recovery is a pure function of the log (the keystone).** On load, run a
+ pure **`reconcile(rows) → cleanHistory`** that deterministically repairs any
+ partial turn:
+ - `tool_call` with no matching `tool_result` → synthesize an error result
+ ("interrupted by shutdown"). This is today's `synthesizeResidualToolResults`
+ logic **moved to the READ path** so it runs on *every* load, not just live.
+ - a turn with no terminal assistant content → mark interrupted; user simply
+ sends the next message to continue.
+ - **Functional-core (P2):** rows in → clean history out, no I/O, exhaustively
+ unit-testable with crafted "crash-shaped" inputs. **Guarantee: whatever a
+ crash leaves, `reconcile` always yields a provider-acceptable history.**
+ "Broken state" becomes a state the rest of the system never observes — it's
+ repaired at the boundary.
+- **R3 — Status is derived, never authoritative.** A persisted `running` flag is a
+ lie waiting to happen. On boot, sweep all `running → interrupted`; AND treat
+ live status as runtime-only (derive "is this tab live?" from "is there an
+ in-process turn driving it?"). A crash can't leave a tab stuck running.
+- **R4 — Resume = load → reconcile → continue.** Because history is append-only
+ and `reconcile` guarantees validity, resuming after *any* failure is identical
+ and invisible to the user — no special "recovery mode". Token-exhaustion and
+ tool-errors already end the turn cleanly and persist (the error becomes a
+ chunk), so they are *already* resumable once R1 closes the crash window.
+
+**Where it lives (fits the architecture):** almost entirely in the
+`conversation-store` **core extension** (R1 incremental write, R2 reconcile-on-load)
++ a tiny **boot sweep** (R3). The **kernel stays pure** — `runTurn` still just
+takes `messages` and emits events; it knows nothing about crashes. `reconcile` is
+the canonical **functional-core** unit (P2) and the highest-value test target in
+the system (feed it every crash shape).
+
+**Cost / boundary (P4):**
+- R1 trades IO for safety (more, smaller transactions vs. one-per-turn — the
+ current code chose one-fsync-per-turn for "constrained backends"). Per-step
+ batching is the mitigation; granularity configurable.
+- **Out of scope here:** resuming a half-finished assistant message *mid-sentence*
+ (wishlist #1 "resume mid-generation" — needs in-flight streaming state). The
+ promise here is narrower and is what's actually wanted: **the history is never
+ broken, and the user can always continue the conversation.** Mid-stream
+ resumption can build on this foundation later.
+
+### 3.5 The hook system (extensible without prediction)
+
+**The goal:** features react to actions in other features (e.g. *"user sent a
+message → reset the cache-warming timer"*). Hooks must be **part of the
+contracts** (typed, stable, exposed) *and* **easy to add later** without
+predicting features that may never exist. Those only conflict if hooks live in a
+central kernel registry — so they don't.
+
+**What today's code already does (the patterns to generalize):**
+- **Observer stream.** `NotificationDispatcher` depends not on `AgentManager` but
+ on a minimal interface — `interface AgentEventSource { onEvent(listener):
+ () => void }` — and wraps every handler so *"a transport bug can never
+ propagate into the agent loop."* That's already a primitive hook contract
+ (subscribe → react → unsubscribe, errors isolated).
+- **Semantic lifecycle calls (a hook in disguise).** Cache-warming exposes
+ `onUserMessage(tabId)` (cancel timer) and `onTurnEnded(tabId)` (re-arm),
+ *called explicitly* from `tabs.svelte.ts`. Hand-wired coupling we want to
+ dissolve into subscriptions.
+
+**The keystone decision — decentralized hook catalog:**
+> The **kernel owns the hook *mechanism*** (`emit`, `on`, `applyFilters`). Each
+> **extension declares the hooks it emits** as part of its own contract. The hook
+> catalog is the *union* of all extensions' declarations — never a central list.
+
+The kernel never enumerates "the hooks that exist." This is what makes "add a
+hook as required" a **local, additive** change instead of a kernel edit.
+
+**The typed descriptor (the contract surface).** A hook is an exported, typed
+descriptor — not a loose string:
+```ts
+// owned by the session-orchestrator (it performs message intake)
+export const MessageReceived = defineHook<{ tabId: string; text: string }>("session/message.received");
+// owned by the KERNEL (it owns the turn loop)
+export const TurnSealed = defineHook<{ tabId: string; turnId: string }>("kernel/turn.sealed");
+```
+Consumers get full type inference, no central enum to edit:
+```ts
+// cache-warming extension (dependsOn session-orchestrator)
+host.on(MessageReceived, ({ tabId }) => cancelTimer(tabId)); // payload inferred
+host.on(TurnSealed, ({ tabId }) => armTimer(tabId));
+```
+The descriptor **is** the contract: importing it gives the id + payload type.
+Adding a hook = exporting one more descriptor from its owner.
+
+**Two hook kinds (and one thing that is NOT a hook):**
+| Kind | Shape | Changes outcome? | Errors | Awaited by turn? | Example |
+|---|---|---|---|---|---|
+| **Event** | fire-and-forget, N listeners | No | **isolated per-handler — never breaks the turn** (today's rule) | No (optional bounded timeout) | `message.received`, `turn.sealed`, `tool.after` |
+| **Filter** | chain, value in → value out, ordered | Yes (in-band) | fail-open + log by default; owner may mark a chain fail-closed | Yes (in-band; a slow filter slows the turn, by design) | system-prompt assembly, tool-result transform |
+
+> **NOT a hook: request/response with exactly one responder** (e.g. "ask the
+> human for permission"). That's a **service** (`host.provideService` /
+> `getService`) — one responder, returns a value. Modeling it as a hook invites
+> "which of N handlers wins?" ambiguity. (Permission-prompting is the tempting
+> thing to mis-call a hook — it isn't one.)
+
+**The workflow you actually care about — "add a hook later":**
+1. Find the **owner** (the extension that performs the action).
+2. Export one descriptor from its contract: `defineHook<Payload>("owner/the.action")`.
+3. Emit at the action site: `host.emit(TheAction, payload)`.
+4. The consumer `dependsOn` the owner and subscribes. **Kernel unchanged.**
+
+The kernel changes *only* when the action is a kernel-intrinsic turn-loop moment
+(e.g. a new `tool.before` phase) — and even then it's **+1 exported descriptor +
+1 emit line**, never a structural change, because the mechanism is generic.
+
+**Decisions baked in now (all grounded, P4):**
+- **Namespacing (P8):** every hook id is `owner/name` (`kernel/turn.sealed`,
+ `session/message.received`) — prevents third-party collisions.
+- **Event error isolation is a hard contract rule** (lifted from
+ `NotificationDispatcher`): a thrown/rejected event handler is caught, logged,
+ dropped — it can *never* fail the turn.
+- **Filter ordering is deterministic:** dependency-topological registration order,
+ with an optional numeric `priority` escape hatch.
+- **Async semantics:** events are not awaited (fire-and-forget, optional bounded
+ timeout); filters *are* awaited (in-band).
+
+**Deliberately NOT built yet (P4 / P6):**
+- No wildcard/pattern subscriptions (`turn.*`) until something needs them.
+- No hook-to-hook dependency graph — registration order + `priority` suffices.
+- **Don't hook every internal function.** A hook exists only where *cross-
+ extension* reaction is a real need (mirrors P6 — expose only what's needed).
+ Over-hooking turns the codebase into spaghetti-by-events.
+
+**The cache-warming example, fully mapped:**
+| Today (coupled) | Target (hooked) |
+|---|---|
+| `tabs.svelte.ts` calls `cacheWarming.onUserMessage(tabId)` | cache-warming does `host.on(MessageReceived, …)`; orchestrator emits it |
+| `tabs.svelte.ts` calls `cacheWarming.onTurnEnded(tabId)` | cache-warming does `host.on(TurnSealed, …)`; kernel emits it |
+| frontend hard-wires the dependency | cache-warming `dependsOn` session-orchestrator; zero call-site coupling |
+
+Both hooks it needs (`message.received`, `turn.sealed`) already have natural
+owners — **no prediction required**, which is the test that the model holds up.
+
+### 3.6 Testability enforcement (design for tests, don't just write them)
+
+**The principle:** don't merely write tests for code — write code *specifically so
+it is testable*. Crucially, this is **not directly machine-enforceable**: a tool
+can catch the *symptoms* of untestable code, never the intent. So the strategy is
+two-pronged — **make the testable path the path of least resistance, then
+mechanically catch the worst regressions.**
+
+**Testability is an OUTPUT of principles we already adopted** — enforce the
+*causes*, not the slogan:
+- **P2 (inject effects)** → code becomes input→output → testable without mocks.
+- **P3 (no ambient state)** → nothing hidden to stub → testable in isolation.
+- **P1 (feature-as-a-library)** → small importable surface → testable standalone.
+
+**Evidence in today's code (the disease we enforce against):**
+`packages/api/tests/agent-manager.test.ts` is **2,142 lines** with a large
+`vi.mock("@dispatch/core")` block — which exists *solely because* `agent-manager.ts`
+reaches for its dependencies instead of receiving them. That is not a testing
+failure; it's a P2/P3 failure that *manifested* in the tests. **Mock count is a
+proxy metric for design quality** — that's the lever. (Today: ~14 test files use
+`vi.mock`; the kernel + each pure-core must reach **zero internal mocks**.)
+
+**The enforcement ladder (cheapest/strongest first):**
+
+- **Tier 1 — Structural (free, mechanical, highest leverage).** The package
+ boundaries we're already building *are* testability enforcement. A feature's
+ decision logic lives in a package with **zero effectful imports** (no
+ `bun:sqlite`, `node:fs`, `node:child_process`) → it is *structurally
+ impossible* to write untestable effectful code there; the imports don't exist.
+ Proven by today's deliberately DB-free `chunks/transform.ts`. **Enforce via a
+ dependency-direction lint** (Biome `noRestrictedImports` forbidding effect
+ modules in pure files). The untestable version *doesn't typecheck* — this is
+ the real answer to "how do we enforce it."
+- **Tier 2 — The no-mock smell test (the proxy metric).** Stated, reviewable rule:
+ *a unit test that needs to mock OUR OWN modules is a design bug, not a test to
+ write.* Allowed: mocking the **outermost edge** (real network, real clock).
+ Banned: mocking `@dispatch/*` internals. Mechanical proxy: a CI grep hard-fails
+ if a **kernel/core** test introduces an internal mock; the global count must
+ trend toward zero.
+- **Tier 3 — Coverage as a FLOOR, not a target (with a caveat).** No coverage
+ tooling exists today — add `@vitest/coverage-v8`. But (P4): coverage is a bad
+ *target* (gameable — 100% of mock-heavy untestable code proves nothing) and a
+ useful *floor* **only on pure-core/kernel packages**, where high coverage is
+ cheap *because* the code is pure. **No global coverage gate** — it would
+ incentivize mock-heavy shell tests, the exact thing we're fighting.
+- **Tier 4 — The harness layer (P5/P6 — teach the agents).** Encode the rule so
+ future agents inherit it: a `rules/` safety reflex (below) + a **testable-by-
+ default extension scaffold** in `sdk/` shipping the split pre-made: `logic.ts`
+ (pure, no deps) + `adapter.ts` (effects) + `logic.test.ts` (mock-free). When
+ the *template* is testable, the default output is testable.
+
+**THE KEY CAVEAT — asymmetric enforcement (strict core, lenient shell).** This is
+itself an application of the AI-harness thesis (P5/P6): **scoped rules beat
+general rules** — models already know "write testable code"; what they need is
+*"this kind of code, in this layer, gets tested this way."*
+- **Pure core / kernel:** strict — zero internal mocks, dependency-direction lint,
+ coverage floor. High coverage is *cheap* here, so demand it.
+- **Imperative shell (orchestrator, transport, real SQLite adapter):** lenient —
+ it will *never* hit high pure-unit coverage, and **forcing it to is the
+ anti-pattern** (you'd do it by mocking everything, recreating today's mess).
+ The shell gets a *thin layer of integration tests* against real / in-memory
+ backends. A blanket rule would backfire — enforcement is asymmetric **by
+ design**.
+
+**`rules/` safety reflexes to ship (Tier 4, scoped per the asymmetry):**
+- *Pure-core/kernel rule:* "Writing a unit test that mocks an internal module?
+ The code is wrong, not the test. Move the decision logic to a pure function and
+ inject the effect."
+- *Pure-core/kernel rule:* "This package must have zero effectful imports
+ (`node:fs`, `bun:sqlite`, `node:child_process`, network). Need an effect?
+ It belongs in the adapter/shell, injected."
+- *Shell rule:* "Don't chase pure-unit coverage here. Write a few integration
+ tests against a real or in-memory backend; do NOT mock sibling extensions."
+- *General (all):* "Mocking the outermost edge (real network/clock) is fine;
+ mocking `@dispatch/*` is a smell — fix the boundary."
+
+**The enforced standard (commit to this):**
+1. Every extension has a **pure core with zero effect-imports**, lint-enforced
+ (Tier 1) — *the load-bearing one.*
+2. **No internal mocks in kernel/core tests** — CI grep; proxy metric → zero (T2).
+3. **Coverage floor on pure packages only**, never global (Tier 3).
+4. **Scoped `rules/` reflexes + a testable-by-default scaffold** (Tier 4).
+
+**Tooling actions (when we start):** add `@vitest/coverage-v8`; add the
+dependency-direction lint (Biome `noRestrictedImports`) scoped to pure packages;
+add the CI internal-mock grep for kernel/core; ship the `sdk/` scaffold.
+
+### 3.7 Trust & isolation model (fault containment, not adversary sandboxing)
+
+**Threat model first (P4 — defend a real threat, not an imported one).** Dispatch
+is **personal, self-hosted, single-operator** today. So:
+- **Malicious extension** (data theft, host attack) — **NOT the current threat.**
+ You run the host and choose the extensions; an installed extension is already
+ as trusted as code you write. The "untrusted plugin marketplace" justification
+ for sandboxing does not apply *yet* (revisit if Dispatch goes multi-tenant or
+ ships a public registry).
+- **Buggy extension** (infinite loop, unhandled rejection, leak, bad migration)
+ taking down every other tab/agent — **REAL and present**, especially since we
+ want external/custom extensions. This directly threatens the §3.4 "never leave
+ the system broken" guarantee.
+
+**So we defend against FAULTS, not ADVERSARIES** — until the project's nature
+changes. That distinction collapses the decision.
+
+**Options considered:**
+- **A — In-process, trusted (no isolation):** simplest/fastest, rich live-object
+ API. But one throw / `process.exit` / leak hits everyone; capabilities are only
+ advisory. *Too little — contradicts §3.4.*
+- **C — Hard isolation (worker/subprocess/VM per extension):** real fault *and*
+ adversary isolation, enforceable capabilities. But **forces the entire Host API
+ to be serializable** — no live `provider` handed to `runTurn`, no closure
+ handlers, no streaming `ctx.onOutput` without marshalling — fighting *every*
+ contract we designed, at real per-call IPC cost. *Too much, too early; defends
+ a threat we don't have, and deforms the contracts (the P4 anti-pattern).*
+- **B — Soft isolation (in-process, defensively wrapped):** keep the rich
+ in-process API, but the host wraps every extension boundary. **CHOSEN.**
+
+**DECISION: adopt B now; design contracts so C remains *possible* later without a
+rewrite.** Concretely:
+- **Host API stays rich/in-process** — live handlers, streams, objects. All prior
+ design holds unchanged.
+- **Every extension boundary is defensively wrapped:** handler try/catch (already
+ §3.5), **mandatory timeouts on awaited filters** (§3.5 makes filters in-band, so
+ a runaway filter must be time-bounded), and **per-extension fault tracking →
+ auto-disable a repeatedly-faulting extension** (contains the fault instead of
+ letting it recur; ties to §3.4).
+- **Tier-aware auto-disable (mirrors the §3.6 asymmetry — strict core, graceful
+ edge):** `standard`/`external` extensions *may* be auto-disabled on repeated
+ faults; **`core`/`kernel` faults are fatal-and-surfaced, never silently
+ degraded** — you want to know storage/transport is broken, not limp on. (Tools
+ also get a deterministic residual result per §3.4 R2, so a tool fault never
+ orphans a turn.)
+- **Capabilities are declared + gate-enforced at the Host-API surface**
+ (advisory-but-checked), NOT OS-sandboxed. Honest scope: this catches accidental
+ overreach and documents intent; it does not stop determined native code.
+- **Cheap future-proofing for optional C later:** keep contract payloads
+ **structured and in-principle serializable** (the typed hook/service handles of
+ §5.4 already push this way) — don't pass arbitrary live object *graphs* between
+ extensions via services. Then moving one untrusted extension into a worker is a
+ localized change, not an architecture rewrite.
+- **Manifest `trust` field** (`bundled` | `local` | `external`) recorded now even
+ though all three behave identically under B — so the *policy hook* exists when
+ we later want to treat `external` differently (e.g. worker isolation) without
+ inventing the concept then.
+
+**Harness rules this decision generates (scoped per §5.1 layered knowledge; write
+into the agent files when those agents are built — NOT now, per §7.4):**
+- *All extension-author agents (shared knowledge):* "Your hook/filter handlers
+ must never throw uncaught — the host wraps them, but a throw burns your fault
+ budget and can auto-disable your extension." / "Filters are awaited and
+ time-bounded — no unbounded work in a filter." / "Assume your extension can be
+ disabled/reloaded independently; don't rely on ambient process state surviving
+ (§3.4)."
+- *Service/contract-defining agents only:* "Keep service/contract payloads
+ structured and serializable-friendly — no passing live object graphs across the
+ extension boundary (preserves the option to isolate later)."
+- *Kernel/core agents only (strict):* "Core/kernel faults are fatal-and-surfaced,
+ NOT auto-disabled — never write graceful-degradation code that hides a
+ storage/transport failure."
+- *Tooling-enforced → deliberately NOT in agent files (P6):* the typed-handle
+ rule (§5.4) is a compile error, and capability over-declaration is caught at
+ manifest load — neither is written down as prose.
+
+---
+
+## 4. Cross-cutting decisions to lock down (when we start)
+
+- **Contract versioning:** convention-only & dormant in `0.x` (§2.9). Each package
+ self-versions; semver *meaning* is changelog hygiene + the §5.3 fan-out signal.
+ Internal safety = the type system; the compat gate / `.d.ts`-diff are deferred
+ until external extensions exist.
+- **Trust & isolation:** **soft isolation (B)** — rich in-process Host API +
+ defensively-wrapped extension boundaries (handler try/catch, filter timeouts,
+ tier-aware auto-disable). Defends FAULTS not adversaries; contracts kept
+ serializable-friendly so hard isolation (C) stays possible later (§3.7).
+- **System prompt / persona:** becomes a context-filter contribution, not a
+ hard-coded string — so the assistant's "feel" is swappable.
+- **Migrations ownership:** each extension owns its tables; the kernel only runs
+ the migration runner. Defines a clean uninstall story.
+- **Deterministic tool-set per turn:** reproducible from `(agent profile +
+ capabilities + active extensions)` — this is P3 made concrete and kills
+ wishlist bugs #16/#17.
+- **Tool-dispatch policy:** togglable per §3.3; default value is an open question
+ (see §8).
+- **Durability / crash recovery:** incremental append + pure `reconcile()` on load
+ + derived status (§3.4). Design rule: no persisted state a crash can leave may
+ be unrepairable — recovery is deterministic and invisible to the user.
+- **Hooks:** decentralized catalog — kernel owns the mechanism, each extension
+ declares the hooks it emits via typed descriptors (§3.5). Events are
+ error-isolated; filters are in-band; single-responder request/response is a
+ service, not a hook.
+- **Testability enforcement:** asymmetric — strict on pure core (zero
+ effect-imports lint, no internal mocks, coverage floor), lenient on the shell
+ (thin integration tests) (§3.6). Mock-of-internals count is the proxy metric.
+- **Agent workflow:** one owner-agent per extension/kernel; agents see only
+ others' contracts, never implementation; contract changes fan out mechanically
+ via `lsp references`; non-static cross-extension coupling is forbidden;
+ glossary terms are human-gated (§5).
+
+---
+
+## 5. Repo & agent workflow conventions (one agent per unit)
+
+The repo's **agent-team structure is isomorphic to its module structure**: agents
+communicate through exactly the same contracts the code communicates through. This
+is Conway's Law made intentional, and it yields a diagnostic property:
+
+> **Friction between agents is a signal of bad architecture.** Constant
+> agent-to-agent messaging ⇒ the contract boundary is wrong. An agent needing to
+> read another's implementation ⇒ that contract is underspecified. The workflow
+> *surfaces* design smells instead of hiding them.
+
+It is not a bolt-on — every row below already exists in this plan:
+
+| This model needs… | …already provided by |
+|---|---|
+| Contracts as the only cross-agent surface | ABI (kernel) + two-sided per-extension contracts (§2.3) |
+| One agent per unit | P1 feature-as-a-library — one library, one owner |
+| Per-agent scoped knowledge | **P7 extension-scoped harness** — an extension's AGENTS.md/rules/glossary *is* its owner-agent's knowledge |
+| Layered knowledge (group → file) | P5 tiered-cache layering (§7.1) |
+| Persistent, messageable agents | Dispatch's own tabs + `send_to_tab` + `summon`/`retrieve` |
+| Bounded cross-agent chatter | the existing `MAX_AGENT_AUTO_WAKES` budget |
+| Orchestrator confirms without reading code | **§3.6 testability** — tests-at-boundaries are the trust mechanism |
+
+The last row is the deepest synthesis: **§3.6 is the orchestrator's verification
+protocol.** It can't read code, so it confirms "everything works" from
+*contracts + test results + build/diagnostics output* — which only works because
+we made the boundaries testable. The keystone equivalence: **P7 harness docs ARE
+the agents' scoped knowledge** — the same artifact, two views; you don't design
+knowledge-scoping separately.
+
+### 5.1 The ownership model
+- **One owner-agent per unit** (each extension, and the kernel). Its file(s) are
+ edited by no one else → single-writer, so a (future) sleeping agent wakes
+ knowing its own code is current.
+- **Knowledge is scoped & layered** (P5/P7): shared group knowledge (e.g. all
+ "frontend" agents) → per-extension knowledge → per-file specifics. An owner
+ loads only its layer, so it is a narrow-domain expert with lean context.
+- **Visibility rule:** an agent sees **only what other extensions
+ expose/require** (their contracts) — never their implementation. Implementation
+ is **not provided by default** (P6/§3.6 caveat #3); *needing* it is a signal
+ the contract is incomplete — fix the contract (or ask the owner), don't grant
+ code access. Corollary: **a contract documents behavior & guarantees a consumer
+ can rely on, not just types** (P6 applied to contracts).
+- **Phase note (P4):** start by **summoning fresh agents per task** — files
+ aren't complex enough to justify warm/persistent agents yet. Persistent
+ *waking* agents (and the wake-time "contract-delta since last active" sync they
+ require) are deferred to **after the rewrite**.
+
+### 5.2 The workflow (build a feature)
+1. User asks the **orchestrator** for feature X. (Orchestrator sees all
+ *contracts*, no implementation.)
+2. **Overlap check first (anti-webhook-reimplementation, §7):** orchestrator
+ consults the GLOSSARY + feature-docs to see whether the capability already
+ exists under a canonical term.
+3. **Boundary decision is the USER's, never silent (resolves §3.6 #5):** if X
+ maps to a new capability, the orchestrator **surfaces "new extension vs.
+ extend an existing one?" to the user** and waits — it never decides
+ granularity itself (this is the exact failure the article warns about; the
+ glossary/feature-docs are the defense, the user is the authority).
+4. Orchestrator **summons the owner-agent(s)** to do the work and **messages any
+ extensions needing changes** (via their owner-agents).
+5. Owners report back; orchestrator confirms via contracts + tests + build.
+6. Clarification questions agent↔agent are *allowed but rare* — everything an
+ agent needs (contracts) is already exposed; a needed question usually means a
+ contract gap.
+
+### 5.3 Contract changes — mechanical blast radius (resolves §3.6 #2)
+A contract change is the one event that legitimately fans out. It is handled
+**mechanically, not by guessing**, via the existing `lsp` tool:
+1. The contract's owner edits it, then runs **`lsp references`** on the changed
+ symbol(s) → the complete set of consuming files.
+2. The owner **reports that file list up to the orchestrator** (it can't see
+ other extensions itself); the **orchestrator dispatches** the affected
+ owner-agents to update to the new contract.
+- **Ownership:** kernel-intrinsic ABI → kernel agent (most conservative, changes
+ rarely). Per-extension contracts → that extension's agent, **co-located in its
+ package** (not a central dir — see §2.5).
+- **Prerequisite:** a **TypeScript language server** wired into `dispatch.toml`
+ (today's LSP config only has the Luau example).
+
+### 5.4 Static-reference rule — non-static cross-extension coupling is forbidden
+For §5.3 to be *sound*, `lsp references` must see every coupling. So:
+
+> **Every cross-extension coupling is anchored to an exported typed symbol.**
+> Dynamic/string-keyed cross-feature references are forbidden.
+
+- **Enforced by the type system, not a lint:** the Host API *accepts only typed
+ handles* — `host.on(HookDescriptor<T>, …)`, `host.getService(ServiceHandle<T>)`
+ — so a raw string at a consumer site is a **compile error** (surfaced via `lsp
+ diagnostics`). The raw string exists in exactly one place: the owner's
+ `defineHook`/`defineService` declaration. `lsp references` on that exported
+ symbol therefore returns the true, complete blast radius. This is *why* typed
+ descriptors (§3.5) + typed service handles (§2.3) beat string lookups — not
+ aesthetics, but making the agent workflow mechanically sound.
+- **Scope (P4 — don't overclaim):** this bans cross-extension **code** coupling.
+ Two dynamic lookups are *legitimate and stay*, because they are **data flow /
+ discovery inside the kernel-host, not feature-to-feature references**:
+ (a) the kernel routing a model's tool-call by name (`byName.get(name)`) — the
+ name is the LLM's runtime choice, i.e. data; (b) the host loading extensions by
+ scanning manifests (traced by the manifest DAG, not symbol refs).
+- **The one escape hatch (named, restricted):** generic observability (e.g. a
+ logger wanting *every* hook) may use a single `host.onAny(listener)` firehose,
+ explicitly marked "observability only, never feature code."
+
+### 5.5 Integration bugs — the temporary multi-knowledge agent
+A bug where X and Y each honor the contract yet don't work together belongs to no
+single file. Resolution (resolves §3.6 #4):
+- The orchestrator dispatches a **temporary multi-knowledge agent** loaded with
+ the **scoped knowledge AND read/write access to the 2–3 relevant files** —
+ unlike normal agents it *does* see implementation, because fixing integration
+ requires it.
+- It becomes the **temporary exclusive owner** of those files for its lifetime
+ (the orchestrator must not let the normal owners edit them concurrently →
+ preserves single-writer).
+- **Both trigger paths:** the orchestrator dispatches it proactively, OR a
+ file-owner who spots the bug **requests one from the orchestrator** (reuses the
+ §3.5 agent→orchestrator message path; no new mechanism).
+- It leverages the existing knowledge-scoping so the agent gets *exactly* the
+ context to fix the seam and no more.
+
+### 5.6 The glossary is a human-gated checkpoint (strengthens P8)
+
+This is the article's central anti-synonym-drift mechanism: the GLOSSARY's
+**"aliases to avoid" column** exists so the agent never reinvents a concept under
+a new name (the article's `WebhookEvent` / `WebhookHook` / `HookedWebhook`
+problem), and the §5.2 step-2 overlap check is *when* it runs ("mandatory feature
+overlap detection before any new feature"). The orchestrator may **never silently
+coin a term.** Two cases:
+
+**Case A — concept already exists (synonym-drift defense — the priority).** When a
+request *describes* an existing concept — even by behavior, under a different name
+— the orchestrator must **recognize the match and steer to the existing canonical
+term, creating NO new entry.**
+- *Example (the user's):* request = "implement a **web-notifier**: accept a
+ request from an HTTP endpoint requiring no password, then log it." The
+ orchestrator recognizes this *is* a **webhook** (already in the glossary) and
+ responds "that's a `webhook` — I'll use that name," rather than adding
+ "web-notifier".
+- Recognition is powered by the glossary's aliases + overlap check, and works on
+ **behavioral descriptions**, not just name matches.
+- **Still suggest-then-confirm (P4):** recognition can misfire (the user may mean
+ something subtly different). The orchestrator *proposes* the match ("this looks
+ like a `webhook` — shall I call it that?"); the user has final say. It never
+ silently collapses a possibly-distinct concept into an existing term. If the
+ user confirms it's a new alias for an existing term, add it to that term's
+ "aliases to avoid" column (don't make a new entry).
+
+**Case B — genuinely new concept (name it well).** When the concept is actually
+new, before adding the entry the orchestrator must:
+1. State the new term and its understanding of what it means.
+2. **Propose a name, defaulting to the standardized / training-baked term**
+ (e.g. "patch" not "Bugfix"; "debounce" not "cooldown-wait"). Rationale (P6): a
+ name models already know costs **zero agent-file/glossary space**, so the
+ glossary only grows entries for genuinely project-specific concepts — it
+ actively fights its own bloat.
+3. **Ask the user** to approve or rename. The user is the final authority: if they
+ prefer a different name, **always go with the user's choice** (record the
+ standard term, if any, under "aliases to avoid"). The "suggest the standard
+ name" rule applies only to a *not-yet-decided* term — never to override a name
+ the user already set.
+
+This keeps the user the authority on the project's vocabulary and makes synonym
+drift impossible at the source — P8 with a mandatory human in the loop, biased
+toward (A) reusing existing terms and (B) names the model already knows.
+
+---
+
+## 6. Current-state map (as of this plan)
+
+Dependency direction is one-way: **`frontend → api → core`**. `core` is already
+framework-agnostic (no Hono/HTTP) — the cleanest existing seam. *(Note: "core"
+here is the **current** package name; under the new model the runtime primitive
+is the kernel and "core" becomes the extension tier — see §2.6.)*
+
+```
+packages/
+│
+├── core/ → @dispatch/core — shared domain logic (the "brain"), framework-agnostic
+│ │ (exported via src/index.ts barrel)
+│ ├── agent/agent.ts agentic LLM loop (streamText + manual tool-call dispatch,
+│ │ dedup, per-line/spill truncation, user-interrupt injection,
+│ │ reasoning-effort, multimodal user content)
+│ ├── llm/
+│ │ ├── provider.ts createProvider() — Anthropic + OpenAI-compatible factories,
+│ │ │ mcp_ tool-name prefix/unprefix
+│ │ ├── anthropic-oauth-transform.ts Claude OAuth request-body transform
+│ │ └── debug-logger.ts DISPATCH_DEBUG_LLM stream/loop/fetch logging
+│ ├── tools/ tool implementations (each createXTool → ToolDefinition)
+│ │ ├── registry.ts createToolRegistry; Zod→JSONSchema + Anthropic normalize
+│ │ ├── read-file.ts, read-file-slice.ts, write-file.ts, list-files.ts
+│ │ ├── run-shell.ts (+ BackgroundShellStore), shell-analyze.ts, bash-arity.ts
+│ │ ├── search-code.ts, web-search.ts, youtube-transcribe.ts (+ BackgroundTranscriptStore)
+│ │ ├── summon.ts, retrieve.ts sub-agent spawn / result collection
+│ │ ├── send-to-tab.ts, read-tab.ts tab-to-tab comms
+│ │ ├── task-list.ts (todo), key-usage.ts, lsp.ts
+│ │ ├── truncate.ts universal tool-output truncator + /tmp spill
+│ │ └── path-utils.ts canonicalize / workdir-containment guard
+│ ├── db/ SQLite (bun:sqlite, XDG data dir)
+│ │ ├── index.ts singleton DB + table DDL/migrations (credentials, api_keys,
+│ │ │ usage_cache, wake_schedule, tabs, chunks, settings)
+│ │ ├── tabs.ts tabs CRUD, short-prefix resolution, positions/status/title
+│ │ ├── chunks.ts append-only chunk log: explode/group rows ↔ messages, usage
+│ │ └── settings.ts key/value settings
+│ ├── chunks/ pure conversation-model transforms (no DB import — shared w/ frontend)
+│ │ ├── append.ts appendEventToChunks / applySystemEvent (stream → Chunk[])
+│ │ └── transform.ts explode/group between Chunk[] and flat ChunkRow log
+│ ├── compaction/index.ts head/tail selection, summary prompt + transcript render
+│ ├── config/ dispatch.toml (global ~/.config + project merge)
+│ │ ├── loader.ts, schema.ts, watcher.ts, index.ts load/validate/hot-reload; configToRuleset
+│ ├── credentials/ claude.ts (OAuth identity/billing), api-keys.ts, opencode.ts,
+│ │ copilot.ts, google.ts, anthropic-betas.ts, store.ts, index.ts
+│ ├── models/ registry.ts (ModelRegistry, key states), catalog.ts,
+│ │ attachments.ts (image/pdf validation + limits), index.ts
+│ ├── skills/ parser.ts, loader.ts, index.ts (skill files → agent injection)
+│ ├── agents/ loader.ts, index.ts (global + .dispatch/agents defs, tool-group expand)
+│ ├── permission/ rules engine: evaluate.ts, service.ts, wildcard.ts, index.ts
+│ ├── lsp/ manager.ts, client.ts, server.ts, language.ts, diagnostic.ts, index.ts
+│ ├── notifications/ ntfy.sh: dispatcher.ts, ntfy.ts, config.ts, types.ts, index.ts
+│ ├── types/index.ts ALL shared contracts: Chunk/ChatMessage, AgentEvent, AgentConfig,
+│ │ ToolDefinition, ToolExecuteContext, DispatchConfig, ReasoningEffort…
+│ └── index.ts public barrel (entire core API surface)
+│
+├── api/ → @dispatch/api — backend HTTP + WebSocket server (Hono on Bun)
+│ ├── index.ts Bun.serve (+ EADDRINUSE port-fallback) + /ws WebSocket
+│ │ (statuses snapshot, event fan-out, permission replies)
+│ ├── app.ts Hono app + CORS; /health, /status, /chat (main entry),
+│ │ /chat/cancel, /chat/stop, /chat/warm; mounts routes;
+│ │ constructs agentManager + permissionManager + notificationDispatcher
+│ ├── agent-manager.ts THE orchestrator (~2.4k lines): per-tab turns, message queue,
+│ │ key/model fallback chain, system-prompt assembly (buildSystemPrompt
+│ │ + TOOL_DESCRIPTIONS), per-turn tool assembly (perm/whitelist gated),
+│ │ sub-agent spawning, LSP-on-write hook, auto-wake budget, compaction
+│ ├── permission-manager.ts tool-permission prompts/replies over WS
+│ ├── wake-scheduler.ts pure Claude wake-probe scheduling helpers (4 slots/hour, recovery)
+│ ├── types.ts thin re-export of AgentEvent/AgentStatus from core
+│ ├── routes/ /config, /tabs, /models (+ startWakeScheduler), /skills,
+│ │ /agents, /notifications (each uses a setXGetter injection seam)
+│ └── tests/ agent-manager, routes, permission-manager, wake-scheduler
+│
+└── frontend/ → Svelte 5 SPA (Vite); morphable, reworked later
+ ├── main.ts, App.svelte, app.css
+ └── lib/
+ ├── tabs.svelte.ts central store: sendMessage + WS event handling
+ ├── ws.svelte.ts WebSocket client (auto-reconnect)
+ ├── router.svelte.ts, config.ts, types.ts, theme.ts, settings.svelte.ts
+ ├── context-window.ts, attachment-tokens.ts, snapshot-sequencer.ts
+ ├── cache-warming.svelte.ts, cache-warm-storage.ts, sidebar-storage.ts
+ └── components/ ChatInput, ChatPanel, ChatMessage, ToolCallDisplay,
+ TabBar, ModelSelector, ConfigPanel, AgentBuilder,
+ SystemPromptPanel, SkillsBrowser, ToolPermissions,
+ PermissionPrompt, TaskListPanel, KeyUsage, CacheRatePanel,
+ ContextWindowPanel, SettingsPanel, MarkdownRenderer, … (23 total)
+```
+
+### 6.1 Key facts that matter for the rework
+- **`agent-manager.ts` is the center of gravity** (~2,453 lines): per-turn tool
+ assembly, system-prompt building, provider/key resolution, sub-agents,
+ queueing all fused. This is what dissolves into kernel + core orchestrator +
+ standard contributions.
+- **`types/index.ts` is the de-facto contract layer today** — `ToolDefinition`,
+ `AgentConfig`, `AgentEvent`, `DispatchConfig` all live here. Natural seed for a
+ real `contracts` package (kernel).
+- **Routes already use a `setXGetter` injection pattern** (`setSkillsGetter`,
+ `setModelsGetter`, …) — a primitive form of the DI seam the extension host
+ would formalize.
+- **Per-turn tool assembly is a giant duplicated if/else** in `agent-manager`
+ (parent-perms path + child-whitelist path) — prime candidate for a registry
+ populated by extensions.
+- **Tool execution today is post-stream + sequential** (`agent.ts` ~line 1426) —
+ see §3.3 for the eager/concurrent redesign.
+
+---
+
+## 7. The AI Harness (meta-information layer)
+
+From "The AI Harness: why your AI coding agent is only as smart as the repo you
+put it in" (Louai Boumediene, Activepieces). Thesis: the model is rarely the
+bottleneck — the structured meta-information around the code is. Agent context is
+a **tiered cache**: tiny files always loaded, big files on demand.
+
+### 7.1 The layering (governing test: P6 — only the non-inferable)
+| Layer | Size / load | Purpose |
+|---|---|---|
+| Root `AGENTS.md` — "constitution" | ~55 lines, **every session** | Non-obvious architecture rules only |
+| Per-package/extension `AGENTS.md` | ~30–55 lines, when working there | Package-specific patterns |
+| `rules/` — "safety reflexes" | 3–5 lines each, every session | Crystallized scar tissue (bugs you've reverted) |
+| `features/*` — "module encyclopedia" | ~60 lines each, on demand | Entity schemas, data flow, gotchas per module |
+| `skills/*` — codified workflows | slash commands, progressive disclosure | Fixed procedures for repeated tasks |
+| `GLOSSARY.md` | term table + "aliases to avoid" | Fights synonym drift |
+
+### 7.2 Why it applies strongly to us (evidence, not fashion)
+- **The layering maps 1:1 onto minimal-kernel + extensions.** "One ~60-line doc
+ per module" *is* "one doc per extension" — the extension boundary is the doc
+ boundary. The architecture gives us the harness structure for free.
+- **We already have the scar tissue that becomes `rules/`:** Anthropic schema
+ normalization in `registry.ts` ("Claude never sees the tool and thinks
+ forever"), workdir-containment in `path-utils.ts`, tool-call dedup ("150+
+ identical calls"), `[USER INTERRUPT]` stripping, the no-`execute` tool pattern.
+ These are postmortems-as-comments — promote them to 3–5 line rules.
+- **Real synonym-drift problem** (P8): tab/session/conversation,
+ chunk/message/turn/step. A glossary with "aliases to avoid" is warranted.
+
+### 7.3 The special angle for this project (synthesis)
+Dispatch is **recursive** — an AI-agent platform that itself *has* skills, agents,
+and permissions. Two consequences:
+- **The harness is extension-scoped (P7):** each extension carries its own
+ constitution snippet, rules, feature doc, glossary terms, and skills, portable
+ with the code. Feature-as-a-library applied to documentation.
+- **"Tiered context as a cache" is already Dispatch's product behavior**
+ (prompt-caching, on-demand skills, compaction). The article describes from the
+ outside the thing we build from the inside — a strong signal the layering is
+ sound.
+
+### 7.4 What we bound or reject (P4 applied)
+- **Volume (40+ docs, 9 skills) and the 5-features/week cadence** — scale
+ artifacts of a 12-engineer, 1.6M-LOC monorepo. Our version: write a doc the
+ moment we touch an extension that lacks one (doc-first as the plan brief), grow
+ organically.
+- **Worktrees / parallel sessions / weekly rhythm / MCPs** — that's *workflow*,
+ not *architecture*; out of scope for the structure we're designing.
+ (Amusingly, Dispatch's parallel tabs are its own take on parallel sessions.)
+
+---
+
+## 8. Open questions / where we start (TBD)
+
+- **Starting point (proposed):** lock the **Contracts** + **Extension Host**,
+ then prove the whole stack with one vertical slice — e.g. extract `read_file`
+ into a standalone, independently-importable `standard` extension with
+ pure-core / injected-shell tests. That single slice validates the architecture
+ (P1, the contracts, the host, the tier model) and the engineering constraints
+ (P2, P3) before scaling out.
+- **Open decisions before we begin:** none remaining — all resolved (see below).
+- **Deferred to after the rewrite (P4):**
+ - Persistent *waking* agents + their wake-time "contract-delta since last
+ active" sync (§5.1) — start with fresh-summoned agents.
+ - TypeScript language server wired into `dispatch.toml` is a **prerequisite**
+ for §5.3's `lsp references` workflow (today only Luau is configured).
+- **Decided so far:**
+ - ~~Tool-dispatch default policy~~ — **DECIDED** (§3.3): default
+ `{ maxConcurrent: 1, eager: true }`.
+ - ~~Who drives the multi-step loop~~ — **DECIDED**: the **kernel** drives it
+ (the loop is the kernel's reason to exist); tools stay dumb objects it calls.
+ - ~~Conversation-store boundary~~ — **DECIDED** (§2.2, §2.8): the kernel keeps
+ only the conversation **model types** + pure transforms; the persistent store
+ and SQLite backend are **`core` extensions** (fixes the §2.2/§2.7 I/O
+ inconsistency).
+ - ~~"Minimum viable turn" target~~ — **DECIDED** (§2.8): `core` targets **(B)**
+ a usable multi-turn chat; the storage backend is the single swappable piece
+ that drops it to the **(A)** stateless floor (= the in-memory test config).
+ - ~~Crash-recovery strategy~~ — **DECIDED** (§3.4): incremental append-only
+ persistence (R1), pure `reconcile(rows)` repair on load (R2), derived/boot-
+ swept status (R3), resume = load→reconcile→continue (R4). Mid-stream
+ resumption (wishlist #1) explicitly deferred.
+ - ~~Hook system shape~~ — **DECIDED** (§3.5): decentralized typed-descriptor
+ catalog (kernel owns mechanism, owners declare hooks); events vs filters;
+ single-responder = service, not hook. Wildcards/pattern-subs deferred.
+ - ~~Testability enforcement~~ — **DECIDED** (§3.6): structural (zero
+ effect-imports in pure packages, lint-enforced) + no-internal-mocks proxy
+ metric + coverage floor on pure packages only + scoped `rules/` reflexes;
+ enforcement is **asymmetric** (strict core / lenient shell).
+ - ~~Agent workflow / repo conventions~~ — **DECIDED** (§5): one owner-agent per
+ unit; contracts are the only cross-agent surface (implementation hidden by
+ default; needing it = contract gap); contract changes fan out via `lsp
+ references` (orchestrator dispatches); **non-static cross-extension coupling
+ forbidden** (typed handles, type-system-enforced, `onAny` escape hatch);
+ temporary multi-knowledge agent for integration bugs; **glossary is
+ human-gated** (orchestrator must ask before coining a term).
+ - ~~Per-extension contract location~~ — **DECIDED** (§2.5, §5): co-located in
+ each extension package; only the kernel ABI is centralized in
+ `kernel/contracts/`.
+ - ~~Boundary granularity (new ext vs extend)~~ — **DECIDED** (§5.2): the
+ **user** decides; the orchestrator surfaces it after a glossary/feature-doc
+ overlap check, never silently.
+ - ~~Trust & isolation model~~ — **DECIDED** (§3.7): **soft isolation (B)** —
+ rich in-process API + defensively-wrapped boundaries; defends faults not
+ adversaries (single-operator threat model); tier-aware auto-disable (strict
+ core / graceful edge); contracts kept serializable-friendly + manifest
+ `trust` field so hard isolation (C) stays possible without a rewrite.
+ - ~~Contract-versioning policy~~ — **DECIDED** (§2.9): convention-only & dormant
+ in `0.x`; each package self-versions; semver meaning (major=break/fan-out,
+ minor=additive, patch=internal) as changelog hygiene + §5.3 signal; type
+ system is the internal mechanism; compat gate + `.d.ts`-diff deferred until
+ external extensions exist.
+ - ~~Core-default provider/auth~~ — **DECIDED** (§2.10): **OpenAI-compatible +
+ API-key** (`provider-openai-compat` + `auth-apikey`) — leanest auth surface,
+ most-testable, and = the **OpenCode Go flash** testbench. Claude/OAuth and the
+ Anthropic-format OpenCode models are `standard` extensions.
+
+---
+
+## Appendix — Principle quick-reference
+- **P1** Feature-as-a-library (importable, minimal API; don't over-split)
+- **P2** Functional core / imperative shell (testability not purity; inject effects)
+- **P3** No ambient state (own and pass explicitly; reproducible tool-sets)
+- **P4** Don't adopt by reputation (earn each pattern against real evidence)
+- **P5** The repo is a harness (meta-info is a first-class, tiered deliverable)
+- **P6** Document only the non-inferable (tribal knowledge / scar tissue only)
+- **P7** The harness is extension-scoped (docs portable with the code)
+- **P8** One canonical vocabulary (glossary + aliases-to-avoid; no synonym drift)
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..dcdf705
--- /dev/null
+++ b/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "dispatch",
+ "private": true,
+ "type": "module",
+ "workspaces": [
+ "packages/*"
+ ],
+ "scripts": {
+ "check": "biome check .",
+ "check:fix": "biome check --write .",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "typecheck": "tsc -b --pretty"
+ },
+ "devDependencies": {
+ "@biomejs/biome": "^2.4.15",
+ "@types/bun": "^1.3.14",
+ "typescript": "^5.7.0",
+ "vitest": "^3.0.0"
+ }
+}
diff --git a/packages/kernel/package.json b/packages/kernel/package.json
new file mode 100644
index 0000000..f613cac
--- /dev/null
+++ b/packages/kernel/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@dispatch/kernel",
+ "version": "0.0.0",
+ "type": "module",
+ "private": true,
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts"
+}
diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts
new file mode 100644
index 0000000..3abab71
--- /dev/null
+++ b/packages/kernel/src/index.ts
@@ -0,0 +1 @@
+export const KERNEL_PLACEHOLDER = true;
diff --git a/packages/kernel/tsconfig.json b/packages/kernel/tsconfig.json
new file mode 100644
index 0000000..2a3be7e
--- /dev/null
+++ b/packages/kernel/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": { "rootDir": "src", "outDir": "dist", "composite": true },
+ "include": ["src/**/*.ts"]
+}
diff --git a/tsconfig.base.json b/tsconfig.base.json
new file mode 100644
index 0000000..591a9e9
--- /dev/null
+++ b/tsconfig.base.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "lib": ["ES2023"],
+ "types": ["bun"],
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+ "noImplicitOverride": true,
+ "exactOptionalPropertyTypes": true,
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "composite": true,
+ "skipLibCheck": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true
+ }
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..e504b9b
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,4 @@
+{
+ "files": [],
+ "references": [{ "path": "./packages/kernel" }]
+}
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..e74aab6
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,7 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ projects: ["packages/*"],
+ },
+});