diff --git a/specs/embedded-agent/design.md b/specs/embedded-agent/design.md new file mode 100644 index 0000000..fac986c --- /dev/null +++ b/specs/embedded-agent/design.md @@ -0,0 +1,229 @@ +# Embedded agent — design + +## What changes + +A chat pane is added to the desktop shell, and an embedded agent runs behind it in the +Electron main process. The agent is a DeepAgents `createDeepAgent` graph, scoped to the open +project, that reads the project with the harness set and writes `wiki/` only through the +validated store. `adr:0019` decided that this may exist; this design is how. + +Three new things, and each is load-bearing: + +1. **A `BackendProtocolV2` implementation that is the guardrail by scope** — + `apps/desktop/src/main/agent/wiki-gate-backend.ts`. It implements `BackendProtocolV2`: + `ls`, `read`, `readRaw`, `write`, `glob`, and `grep` (required) and `edit` (inherited from + the v1 protocol); `delete` is optional and is not implemented (the custom `delete_page` + tool uses the access primitive below, not `backend.delete`). (The bare `BackendProtocol` + export is the deprecated v1 alias; `WikiGateBackend` implements v2.) Reads + (`ls`/`read`/`readRaw`/`glob`/`grep`) operate on the real project directory, every path + confined with `assertWithin(projectRoot)` from `@open-wiki/access`. Writes (`write`/`edit`) + accept only paths that resolve inside `/wiki/` and route them through the + store: `gateWrite` to validate, then `writePage(projectRoot, pagePath, content, "agent")` to + write atomically, log the operation with origin `agent`, and leave it undoable. `edit` is a + logical replacement, not a partial write: the backend reads the page, applies the + exact-string replacement (`replace_all` supported), and writes the full resulting page + atomically through `writePage` — the gate validates the whole new page, not the diff. A + write to any other path returns an error and writes nothing. `execute` is not implemented, + so `WikiGateBackend` is not a sandbox backend (`isSandboxBackend` is false) and the + DeepAgents middleware filters the `execute` tool. + + The built-in `write_file`/`edit_file` tools are **kept and re-pointed** at this backend, + not removed. ADR 0019 excludes "an agent toolkit's filesystem surface — `write_file`, + `edit_file`, `execute`" as "a second writer with direct disk access." The exclusion is + the *direct disk access*, not the names: backed by the gate, `write_file` and `edit_file` + are the same door the editor uses (`writePage` with an origin), and `edit_file`'s + exact-string replace is the optimized edit the product wants — it sends `old_string` and + `new_string`, never the whole page. `execute` and `task` stay excluded (R4.4): `execute` + because the backend does not implement it (the middleware then hides it; shell escapes any + path rule anyway — the middleware itself refuses to combine `permissions` with an + execution-capable backend), `task` because `adr:0019` names subagents dangerous and v1 is + one agent. + + The middleware's own large-result eviction is also confined by this. When a tool result + exceeds the token threshold, the filesystem middleware evicts it by calling + `backend.write("/large_tool_results/.txt", …)` (and human messages to + `/conversation_history/`) — the *same* `WikiGateBackend.write`. Those paths lie outside + `wiki/`, so the gate rejects them: the write returns an error, no file is created, and the + model receives a truncated preview plus "the result could not be saved." The eviction path + therefore fails closed through the gate — it is not a second writer. (If `createDeepAgent` + exposes the eviction threshold, set it to `null` to skip the attempt entirely; either way + the gate is the line.) + +2. **The agent construction in `apps/desktop/src/main/agent/agent.ts`** — + `createDeepAgent` with: `model = new ChatGroq({ model: , apiKey })` read + from `readSecrets` (the same Groq key the recorder uses); `backend = wikiGateBackend`; + `systemPrompt` = the project's harness entry file, resolved at runtime and carried in + unchanged (today `CLAUDE.md`; `plans/harness-portability.md` will write an entry file per + harness — Codex, opencode, Claude — and a project may carry several, as renderings of one + convention, so the resolver reads one — chosen deterministically from the scaffold metadata + or the active harness, rejecting ambiguity before the agent is constructed — and does not + duplicate); `skills = [".claude/skills/"]` + (the shared skills location `adr:0015` chose, loaded by the middleware's `read_file` from + the same backend); `checkpointer = new MemorySaver()` keyed by a `thread_id` per + conversation; `interruptOn` set for `write_file`, `edit_file`, `rename_page`, `delete_page`; + `tools = [renamePageTool, deletePageTool]` (the two custom tools, as tool objects — `tools` + takes tool objects, not a string allowlist). + + The `task` (subagent) tool is removed at construction, not merely unused: `createDeepAgent` + auto-adds a general-purpose subagent — which provides `task` — unless the harness profile + disables it, and `subagents: []` alone does **not** suffice. So a Groq harness profile is + registered once, at module load, via the exported `registerHarnessProfile("groq", + createHarnessProfile({ generalPurposeSubagent: { enabled: false } }))` — the first argument + is the provider key `createDeepAgent` resolves for the model (`getModelProvider(ChatGroq)`, + expected to be `"groq"`; the implementer confirms), so an unknown model that would otherwise + fall back to `EMPTY_HARNESS_PROFILE` instead gets this profile. With the general-purpose + subagent disabled, the `task` tool is never built. `execute` is never built because + `WikiGateBackend` is not a sandbox backend. The filesystem tools (`ls`, `read_file`, + `write_file`, `edit_file`, `glob`, `grep`) are auto-attached by `createDeepAgent` from the + harness profile; they are re-pointed at `WikiGateBackend` because that is the `backend`, so + `write_file`/`edit_file` route through the gate. The convention is carried in, never + re-authored — `generateClaudeMd` and `scaffoldSkills` already write the on-disk files the + agent reads, and the harness-portability plan writes the same convention at each harness's + paths. + + No tracing or telemetry is enabled for the agent's runs. The agent path sets + `LANGCHAIN_TRACING_V2=false` (and unsets `LANGSMITH_*`) before the agent's dependencies are + imported — at the start of main, ahead of any `langchain` / `@langchain/*` import — and + reads no `LANGCHAIN_*` / `LANGSMITH_*` environment variable; project content the agent reads + is sent only to Groq. LangChain/LangGraph can auto-initialize LangSmith from those env vars + at import time, before the agent path runs — disabling tracing before the import is what + closes that, not merely refusing to read the vars in our own code. (A developer who sets + them globally would otherwise export the project directory to a third party.) + +3. **The IPC surface and the chat pane** — new channels in + `apps/desktop/src/main/channels.ts`: `chat:send`, `chat:resume`, `chat:cancel`, and a + push channel `chat:event`. `agent.ts` runs `agent.streamEvents(input, { version: "v3" })` + and forwards token, tool-call, and interrupt events to the renderer over `chat:event`. + The renderer's `Chat.tsx` (a new pane component, sibling of `Sources.tsx`) registers in + `navigation.ts` (widen `Pane`), `Rail.tsx` (`PANES`), and `App.tsx` (the pane switch), + following the `reloadKey` + `bridge()` + live-guard pattern the other panes use. The + interrupt event renders the proposed write and the approve/reject/edit controls; the + user's decision comes back over `chat:resume` as a DeepAgents `Command({ resume: { + decisions } })`. + +The credential is reused, not duplicated: `readSecrets` already returns the Groq key; +`settings.ts` already validates it against `/models`. The settings screen gains the +two-purpose notice and the curated model list. `stack.md` gains `deepagents`, +`@langchain/groq`, `@langchain/langgraph`, `langchain`, and `@langchain/core`. + +Serves R1.1, R1.2, R1.3, R1.4, R1.5, R2.1, R2.2, R2.3, R2.4, R2.5, R2.6, R3.1, R3.2, R4.1, +R4.2, R4.3, R4.4, R4.5, R4.6, R5.1, R5.2, R5.3, R5.4, R5.5, R6.1, R7.1, R7.2. + +## Boundaries and contracts + +- **Process boundary.** The agent runs in main; the renderer only sends messages and + renders events. Keys never cross to the renderer (the existing rule in `settings.ts`). + The renderer has no `fetch` and the CSP is `default-src 'none'`; the Groq call happens in + main, so no CSP change is needed. +- **The write boundary is the store, not a second writer.** `write_file`/`edit_file`/ + `rename_page`/`delete_page` call `gateWrite` + `writePage`/`supersedePage` + `appendOperation` + from `@open-wiki/access` — the same path the editor and the hooks use. The agent never + calls `atomicWrite` or `node:fs` directly. The `Origin` on every write is `"agent"`. +- **The read boundary is `assertWithin`.** Every read path is resolved and checked against + the project root with the same `assertWithin` `packages/mcp` uses; a path that escapes + (including a symlink or junction inside the project that points outside — the realistic + escape on Windows, the only supported platform) throws `OutsideProjectError`, which the + backend returns as a tool error. The harness entry file (the agent's system prompt) is + resolved and read in main with the same `assertWithin` + real-path check, not a bare + `node:fs` read; the scaffolded skills are loaded by the middleware's `read_file` through the + backend, so they are confined too. +- **IPC contract.** `chat:send({ text, thread_id })`, `chat:resume({ decisions, interrupt_id, + run_id })`, `chat:cancel({ run_id })`, and push `chat:event({ kind, thread_id, run_id, ... })` + where `kind` is `token` | `tool` | `interrupt` | `done` | `error`, each carrying the fields + its kind requires (an `interrupt` carries the tool, file path, old/new content or the full + resulting page for `replace_all`, or the rename/delete target, plus an `interrupt_id` and a + content hash of the page at interrupt time; a `tool` event carries the call and its result). + One `MemorySaver` — and one agent instance — is scoped to the project window, keyed by + `thread_id`. Typed in `bridge.ts` `OwBridge`; the preload parity check (`preload.ts:104`) and + `dispatch`'s unknown-channel throw (`ipc.ts`) enforce completeness. + +## Data + +- **`Origin`** — the `Origin` union in `@open-wiki/access` (today `"editor" | "cli" | "hook" + | "observer"`) is extended with `"agent"`, a string variant alongside the existing ones, + so the operation log distinguishes an agent write. The log and undo machinery + (`appendOperation`, `undo`) are unchanged. +- **Gated, atomic rename and delete primitives.** `@open-wiki/access` exports no + `deletePage` or `renamePage`; the desktop's `deletePage` (`apps/desktop/src/main/edit.ts`) + calls `node:fs.rmSync` directly and hardcodes `origin: "editor"`, bypassing the gate. New + `deletePage(projectRoot, pagePath, origin)` and `renamePage(projectRoot, oldPath, newPath, + origin)` primitives are added, each a single atomic operation: snapshot the affected pages, + gate + write, record one operation with the given origin, and roll back on failure — no + half-applied rename. Deletion is supersession (the page is marked superseded, not unlinked, + so it stays visible in history and is one undo). `renamePage` writes the new page through + `gateWrite` + `writePage` and marks the old superseded in the same operation, refuses to + clobber an existing target, and refuses the wiki's index, changelog, and log (`gateWrite` + passes those `NON_ENTITY_PAGES` with no content validation — R4.6 closes that for the + agent). `deletePage` refuses the same set. +- **Conversation state** — a `MemorySaver` holding the LangGraph thread per `thread_id`. + In memory only for v1; not on disk; not in the project directory. +- **Interrupt payload** — the proposed write: `{ tool, file_path, old_string?, new_string?, + content? }`, rendered as a diff in the pane, plus a content hash of the page at interrupt + time. For `edit_file` with `replace_all`, the payload carries every match site (or the full + resulting page), so the human sees the complete effect of the tool call, not only the two + strings — a short `old_string` that matches in several places must not be smuggled past + review. Resume carries `decisions: [{ type: "approve" | "reject" | "edit", ... }]` and the + `interrupt_id` it answers. On resume the backend revalidates the page against the stored + hash; if another writer changed it in the window between interrupt and resume, the stale + edit is not applied — the run re-interrupts with a fresh proposal (R5.5). + +## Alternatives considered + +- **Own `create_page`/`edit_page` tools instead of re-pointing `write_file`/`edit_file`.** + Rejected for v1: the product wants the optimized exact-string edit DeepAgents already + ships, and re-pointing the built-in tools at a gate-backed backend reuses the middleware + (line numbering, large-result eviction, permission filtering) instead of rebuilding it. + The ADR's exclusion is honored by what the backend does (route through the store), not by + the tool names. The large-result eviction, re-pointed at the same backend, is itself + gate-confined — it cannot reach disk outside `wiki/`. +- **`FilesystemBackend` with `permissions` for read-only project + a separate gate tool.** + Rejected: `FilesystemBackend` writes to real disk, so `write_file`/`edit_file` would be a + second writer unless separately disabled, and `permissions` is permissive when no rule + matches (the failure mode `adr:0019` names). A custom backend makes confinement + structural — there is no permissive default to misconfigure. +- **A separate `packages/agent` workspace package.** Deferred: the runtime is desktop-only + for v1 (it lives in main and reads the desktop-held credential). It stays in + `apps/desktop/src/main/agent/` alongside `recorder.ts` and `transcribe-run.ts`; it can be + extracted when a second consumer appears. +- **Including `execute` and `task`.** Rejected: `execute` cannot be scope-guarded (shell + escapes path rules), `task`/subagents are dangerous by `adr:0019`. Decided with the user + at spec time. `task` is removed by disabling the general-purpose subagent in the Groq + harness profile (the framework's own switch), not by a custom filter. + +The hard-to-reverse choice — adopting DeepAgents as the harness — is already recorded in +`adr:0019`, which names `deepagents@1.12.1` and its allowlist. No new ADR is needed; the +gate-backed backend is reversible (it is our code, not a framework commitment). + +## Risks + +- **A well-formed and wrong page passes the gate.** This is `adr:0019`'s stated cost; the + mitigation is the human-in-the-loop approval (R5), not the gate. The interrupt shows the + proposed change so a human can reject a plausible-but-wrong page. +- **The system prompt is project-controlled.** The agent's instructions are the project's + `CLAUDE.md` and skills, read from disk. Opening an untrusted project with the agent + enabled is the trust decision: a malicious `CLAUDE.md` is the agent's rules, not just the + content it reads, and it can instruct well-formed wrong pages that pass the gate. This is + the accepted cost of carrying the convention in unchanged; human-in-the-loop is the only + mitigation. The agent is the lesser door, not a harness. +- **The wiki's index, changelog, and log are `NON_ENTITY_PAGES`.** `gateWrite` passes them + with no content validation — they are "themselves." R4.6 protects them against + `rename_page`/`delete_page` (structural removal or replacement) but not against + `write_file`/`edit_file`: an agent content edit to `wiki/index.md` is gated only by human + approval, not by the gate's form checks. This is intentional — maintaining the index and the + changelog is part of wiki maintenance, so the agent may edit them with approval, but may not + rename or delete them. +- **Tracing exfiltration.** LangChain/LangGraph ship LangSmith tracing that activates on + `LANGCHAIN_*` / `LANGSMITH_*` env vars and would send prompts, tool calls, and tool results + (project content) to a third party. The agent path reads none of those env vars and sets no + tracing client (R2.6); a user who sets them globally does not expose the agent's runs. +- **The DeepAgents `BackendProtocol` is an internal interface.** It is exported but not + versioned as a stable public API; a `deepagents` upgrade could change it. Pinned in + `package.json`; the proof tests (R6.1) would fail on a behaviour change, which is the + signal to update. +- **Skills/`CLAUDE.md` shapes diverge.** `adr:0019` names this as luck, not foresight; + `SKILLS_VERSION` is the only staleness signal. Carrying in unchanged means a divergence + is silent; the agent would still run, reading the older shape. +- **Two writers of the wiki.** The external harness and the embedded agent share one + convention (the generated files) but nothing enforces they agree at runtime. The + operation log's `origin` field is the audit trail; concurrent writes are the user's to + sequence. \ No newline at end of file diff --git a/specs/embedded-agent/requirements.md b/specs/embedded-agent/requirements.md new file mode 100644 index 0000000..a0e82bb --- /dev/null +++ b/specs/embedded-agent/requirements.md @@ -0,0 +1,129 @@ +--- +autonomy: auto +ci: wait +--- + +# Embedded agent — requirements + +Enabled by `adr:0019-an-embedded-agent-that-reads-freely-and-writes-through-the-gate`, +which narrowed `adr:0013` to allow the application to run an embedded agent. This spec +is the first thing that record's "Consequences" section calls for. + +## Purpose + +A chat pane in the desktop application, backed by a DeepAgents agent that reads the open +project the way a harness does and writes the wiki only through the validated store. It is +the lesser door, for the user who downloaded the installer and has no harness — not an +equivalent to one. The agent consults (list, glob, grep, read) and maintains the wiki +(create, edit, rename, delete), with every wiki write paused for human approval before it +lands. + +## R1 · The chat pane + +- **R1.1** The desktop application shall present a chat pane in the shell rail, alongside + wiki, sources, and checks. +- **R1.2** While a Groq credential is configured, the chat pane shall let the user send a + message and shall stream the embedded agent's response into the pane. +- **R1.3** While no Groq credential is configured, the chat pane shall show an empty state + that names the Groq key as the requirement and links to settings, and shall not accept a + message. +- **R1.4** If an agent run errors, then the chat pane shall surface the error in place and + preserve the conversation that produced it. +- **R1.5** The chat pane shall replace the "there is no model behind this window" copy + shipped today with the empty state in R1.3, in any release that carries this feature. + +## R2 · The embedded agent runtime + +- **R2.1** The embedded agent shall run in the Electron main process, scoped to the open + project directory, and the renderer shall reach it only over IPC. +- **R2.2** The embedded agent shall use Groq as its model, via `ChatGroq`, with the same + Groq credential the recorder uses for transcription; no second credential shall be + introduced. +- **R2.3** The embedded agent's instructions shall be the project's harness entry file — + the file the project was scaffolded for its harness to read — and the scaffolded skills, + carried in unchanged; no hand-written system prompt shall be added beside them. A project + scaffolded for more than one harness carries more than one entry file, and they are + renderings of one convention, so the embedded agent shall resolve one deterministically — + from the scaffold metadata or the active harness, rejecting ambiguity before construction — + and shall not duplicate the convention in its prompt. +- **R2.4** While the project has no Groq credential, the desktop application shall disable + the embedded agent and state in the settings screen that a Groq key is required for the + agent and serves transcription and the agent both. +- **R2.5** The settings screen shall offer a curated model selection for the agent, not the + raw provider model list, with one default chosen for tool-calling reliability. +- **R2.6** The embedded agent shall send project content only to Groq; it shall not read + tracing or telemetry environment variables, shall set tracing disabled before the agent's + dependencies are imported (so library-level auto-initialization does not fire), and shall + not transmit prompts, tool calls, or tool results to any third party. + +## R3 · Reading — unrestricted within the project + +- **R3.1** The embedded agent's read tools (`ls`, `read_file`, `glob`, `grep`) shall read + the project directory, and every path they touch shall be confined with `assertWithin` to + the project root. +- **R3.2** If a read path resolves outside the project, then the embedded agent shall return + an error and deny the request. + +## R4 · Writing — only through the gate + +- **R4.1** The embedded agent's `write_file` and `edit_file` tools shall write into `wiki/` + only, through the validated store — `gateWrite` then `writePage` with origin `agent` — + which validates frontmatter, resolves wikilinks and citations, writes atomically, logs the + operation, and leaves it undoable. +- **R4.2** The embedded agent shall have `rename_page` and `delete_page` tools that act on + wiki pages through the validated store — `gateWrite`, then `writePage` or `supersedePage`, + then `appendOperation`, with origin `agent` — using a gated delete primitive in + `@open-wiki/access`. The desktop's existing `deletePage` writes with `node:fs` and a + hardcoded origin, so it shall not be reused. +- **R4.3** If a write path resolves outside wiki, then the embedded agent shall return an + error and write nothing. +- **R4.4** The embedded agent shall not expose the `execute` (shell) or `task` (subagent) + tools; no shell command shall run and no subagent shall spawn from the embedded agent. +- **R4.5** The embedded agent shall carry the origin `agent` on every wiki write it makes, + recorded in the operation log, so a bad run is one undo rather than an archaeology. +- **R4.6** The `rename_page` tool shall refuse to clobber an existing page, and the + `rename_page` and `delete_page` tools shall refuse to operate on the wiki's index, + changelog, and log pages. + +## R5 · Human-in-the-loop approval + +- **R5.1** When the embedded agent calls a write tool, the embedded agent shall pause the + run before the write executes and wait for a human decision. +- **R5.2** While a write is paused, the chat pane shall show the complete effect of the + proposed change — the page slug, and the old and new content (or the rename/delete target) + — and for an `edit_file` with `replace_all` shall show every match site (or the full + resulting page), and shall offer approve, reject, and edit. +- **R5.3** When the user rejects a paused write, the tool shall not execute and the agent + shall be told the write was rejected and not to retry it unless asked. +- **R5.4** When the user edits the arguments of a paused write, the tool shall execute with + the edited arguments, and the gate shall validate them as if the agent had proposed them. +- **R5.5** When the user resumes a paused write, the embedded agent shall revalidate that the + target page is unchanged since the interrupt and, if it has changed, shall return a fresh + proposal rather than apply a stale edit. + +## R6 · The line is proved, not configured + +- **R6.1** The embedded agent shall be unable to write to disk except through the validated + store, so that no permissive configuration can let a write escape the gate; the `execute` + and `task` tools shall not be exposed. + +## R7 · Conversation state + +- **R7.1** The embedded agent shall keep conversation state in memory, keyed by a thread id, + one per conversation, and shall resume a paused run from the same thread. +- **R7.2** The chat channels shall carry a thread id and a run id, one conversation per + project window, and a resume shall reference the interrupt id it answers; the push events + shall be discriminated by kind — token, tool, interrupt, done, or error — each with the + fields its kind requires. + +## Out of scope + +- Durable conversation persistence across application restarts (in-memory for v1; a + checkpointer on disk is a later spec). +- Subagents (`task`) and shell execution (`execute`) — excluded by R4.4 and by `adr:0019`. +- A second LLM provider (OpenAI, Anthropic). Groq only for v1, per `adr:0019`'s "second + credential purpose." +- MCP-over-HTTP (`adr:0018`, unbuilt). The embedded agent reads the project directly; it does + not go through the MCP server. +- Re-authoring the convention. The system prompt stays the generated `CLAUDE.md` plus the + scaffolded skills. \ No newline at end of file diff --git a/specs/embedded-agent/tasks.md b/specs/embedded-agent/tasks.md new file mode 100644 index 0000000..73cb57e --- /dev/null +++ b/specs/embedded-agent/tasks.md @@ -0,0 +1,65 @@ +# Embedded agent — tasks + +## 1 · The wiki-gate backend + +- [ ] 1.1 (Unit) Implement `WikiGateBackend.ls` over the project directory, confining every path with `assertWithin(projectRoot)` from `@open-wiki/access` — R3.1, R3.2 +- [ ] 1.2 (Unit) Implement `WikiGateBackend.read` (offset/limit, line-numbered) over the project directory, confined with `assertWithin` — R3.1, R3.2 +- [ ] 1.3 (Unit) Implement `WikiGateBackend.glob` and `grep` (literal, passage-level) over the project directory, confined with `assertWithin` — R3.1, R3.2 +- [ ] 1.4 (TDD) Implement `WikiGateBackend.write` to accept only paths inside `wiki/` and route them through `gateWrite` + `writePage(projectRoot, pagePath, content, "agent")` + `appendOperation`; watch red on a non-wiki path, then green when it returns an error and writes nothing — R4.1, R4.3, R4.5, R6.1 +- [ ] 1.5 (TDD) Implement `WikiGateBackend.edit` as exact `old_string`→`new_string` replace (`replace_all` supported) over a wiki page, routing through the store; assert it never rewrites the whole page and that a non-wiki path fails — R4.1, R4.3, R6.1 +- [ ] 1.6 (Unit) Omit `execute` from `WikiGateBackend` so it is not a sandbox backend and the DeepAgents middleware hides the `execute` tool — R4.4 +- [ ] 1.7 (Unit) Extend the `Origin` union in `@open-wiki/access` with the string variant `"agent"`, alongside `"editor" | "cli" | "hook" | "observer"`, so the operation log distinguishes an agent write — R4.5 +- [ ] 1.8 (Unit) Add gated, atomic `deletePage(projectRoot, pagePath, origin)` and `renamePage(projectRoot, oldPath, newPath, origin)` primitives to `@open-wiki/access`, each a single operation (snapshot affected pages, gate + write, one log entry, rollback on failure); deletion is supersession; the desktop's existing `deletePage` calls `node:fs` with a hardcoded origin and shall not be reused — R4.2, R4.5 +- [ ] 1.9 (Unit) Implement `WikiGateBackend.readRaw` (confined with `assertWithin`, returning `FileData` as a `ReadRawResult`) — the required `BackendProtocolV2` read method — R3.1, R3.2 + +## 2 · The agent runtime + +- [ ] 2.1 (Unit) Build the agent in `apps/desktop/src/main/agent/agent.ts` with `createDeepAgent`: `model = new ChatGroq(...)` from the Groq credential and the curated default model, `backend = WikiGateBackend`, `checkpointer = new MemorySaver()` keyed by `thread_id`, `systemPrompt` = the resolved harness entry file, `skills = [".claude/skills/"]`, and `tools = [renamePageTool, deletePageTool]` as tool objects; the filesystem tools auto-attach from the harness profile and re-point at the backend — R2.1, R2.2, R2.3, R2.5, R4.4, R7.1 +- [ ] 2.2 (Unit) Resolve the project's harness entry file (today `CLAUDE.md`, plural once `plans/harness-portability.md` lands) deterministically — from scaffold metadata or the active harness, rejecting ambiguity — and read it with `assertWithin` + a real-path check; use it as `systemPrompt` and the scaffolded skills as `skills`, both carried in unchanged; assert no hand-written prompt is appended beside them — R2.3 +- [ ] 2.3 (Unit) Implement `rename_page` and `delete_page` tools over the new atomic `renamePage`/`deletePage` primitives with origin `"agent"`; `rename_page` refuses to clobber an existing target and refuses the wiki index, changelog, and log; `delete_page` refuses the same set — R4.2, R4.5, R4.6 +- [ ] 2.4 (Unit) Register a Groq harness profile via `registerHarnessProfile("groq", createHarnessProfile({ generalPurposeSubagent: { enabled: false } }))` — the first arg is the provider key `createDeepAgent` resolves for the model — so the general-purpose subagent (and the `task` tool) is never constructed; assert `subagents: []` alone does not remove `task` — R4.4 +- [ ] 2.5 (Unit) Set `interruptOn` for `write_file`, `edit_file`, `rename_page`, `delete_page`, and emit a `chat:event` interrupt carrying the proposed change — R5.1 +- [ ] 2.6 (Unit) Stream `agent.streamEvents(input, { version: "v3" })` and forward token and tool-call events to the renderer; resume a paused run with `Command({ resume: { decisions } })` on the same `thread_id`, referencing the `interrupt_id` — R1.2, R5.3, R5.4, R5.5, R7.1, R7.2 +- [ ] 2.7 (Unit) Run all agent invocations in the desktop main process; assert the renderer cannot reach the model or the credential directly — R2.1 +- [ ] 2.8 (Unit) Disable tracing/telemetry for the agent's runs — set `LANGCHAIN_TRACING_V2=false` and unset `LANGSMITH_*` before the agent's dependencies are imported (at the start of main), and read no `LANGCHAIN_*` / `LANGSMITH_*` env var, so library-level auto-initialization does not fire and project content is sent only to Groq — R2.6 + +## 3 · IPC channels + +- [ ] 3.1 (Unit) Add `chat:send({text, thread_id})`, `chat:resume({decisions, interrupt_id, run_id})`, `chat:cancel({run_id})` and the `chat:event({kind, thread_id, run_id, ...})` push channel (discriminated by `kind`: token/tool/interrupt/done/error) in `channels.ts`; expose in `preload.ts`, type in `bridge.ts` `OwBridge`, and route in `dispatch` — R1.2, R5.2, R5.3, R5.4, R5.5, R7.2 +- [ ] 3.2 (Unit) Bind the push channel through the buffered `send()` pattern so events before `did-finish-load` queue rather than drop — R1.2 + +## 4 · The chat pane + +- [ ] 4.1 (Unit) Widen `Pane` in `navigation.ts`, add the chat entry to `PANES` in `Rail.tsx`, and render `` in the `App.tsx` pane switch — R1.1 +- [ ] 4.2 (Unit) Build `Chat.tsx` with `bridge()` + `useEffect` + live-guard, sending over `chat:send` and rendering streamed tokens and tool calls from `chat:event` — R1.2, R1.4 +- [ ] 4.3 (Unit) Render the interrupt payload (slug, old/new content or the full resulting page for `replace_all`, or the rename/delete target) with approve, reject, and edit controls, dispatching `chat:resume` with the `interrupt_id`; show a fresh proposal when the backend reports the page changed since the interrupt — R5.1, R5.2, R5.3, R5.4, R5.5 +- [ ] 4.4 (Unit) Show the empty state naming the Groq key requirement and linking to settings while no credential is configured, and disable the composer; this replaces the "there is no model behind this window" copy — R1.3, R1.5 +- [ ] 4.5 (Unit) Surface a run error in place and preserve the conversation that produced it — R1.4 + +## 5 · Credential, model, settings + +- [ ] 5.1 (Unit) Read the Groq key from `readSecrets` for the agent; assert no second credential store is added — R2.2 +- [ ] 5.2 (Unit) Add the curated model selection to the settings screen, with one tool-calling-reliable default — R2.5 +- [ ] 5.3 (Unit) Add the two-purpose notice (transcription + agent) and the whisper.cpp no-agent notice to the settings screen — R2.4 + +## 6 · The line is proved + +- [ ] 6.1 (TDD) Test that `write_file` to a path outside `wiki/` fails and writes nothing — R4.3, R6.1 +- [ ] 6.2 (TDD) Test that `edit_file` to a path outside `wiki/` fails and writes nothing — R4.3, R6.1 +- [ ] 6.3 (TDD) Test that `execute` is not in the agent's tool set and a call is rejected — R4.4, R6.1 +- [ ] 6.4 (TDD) Test that `task`/subagents are not registered (the general-purpose subagent is disabled for the Groq profile) — R4.4, R6.1 +- [ ] 6.5 (TDD) Test that a `write_file` whose content the gate rejects (bad frontmatter, dangling wikilink) is denied and writes nothing — R4.1, R6.1 +- [ ] 6.6 (TDD) Test that a read outside the project root — including a symlink or junction inside the project that points outside — returns an error and reads nothing — R3.2, R6.1 +- [ ] 6.7 (TDD) Test that an approved write lands through the store (validated, logged with origin `agent`, undoable) and a rejected write does not — R4.1, R4.5, R5.3 +- [ ] 6.8 (TDD) Test that a write tool call always emits an interrupt before any `writePage`/`supersedePage`/`deletePage` call — R5.1, R6.1 +- [ ] 6.9 (TDD) Test that a `replace_all` edit's interrupt shows every match site, and that an approved `replace_all` lands exactly as shown and alters nothing else — R5.2, R6.1 +- [ ] 6.10 (TDD) Test that `rename_page` refuses to clobber an existing target, and that `rename_page` and `delete_page` refuse the wiki index, changelog, and log — R4.6, R6.1 +- [ ] 6.11 (TDD) Test that a tool result large enough to trigger the middleware's eviction creates no file on disk (under `/large_tool_results/` or `/conversation_history/`) — the gate rejects the eviction path — R4.3, R6.1 +- [ ] 6.12 (TDD) Test that constructing the agent with `LANGCHAIN_*` / `LANGSMITH_*` env vars set constructs no tracing client and sends nothing to a third party — R2.6, R6.1 +- [ ] 6.13 (TDD) Test that `readRaw` of a path outside the project returns an error and reads nothing — R3.2, R6.1 +- [ ] 6.14 (TDD) Test that resuming a paused write against a page changed since the interrupt does not apply the stale edit and re-interrupts with a fresh proposal — R5.5, R6.1 + +## 7 · Docs + +- [ ] 7.1 (Unit) Add `deepagents`, `@langchain/groq`, `@langchain/langgraph`, `langchain`, and `@langchain/core` to `docs/stack.md` with one line each on why — R2.2 +- [ ] 7.2 (Unit) Add canonical terms (embedded agent, chat pane, wiki-gate backend) to `docs/glossary.md` with the synonyms to avoid — R1.1, R2.1 \ No newline at end of file