# thClaws — Technical Manual

Engineering reference for thClaws contributors, advanced users, and operators. Different audience than [`user-manual/`](../user-manual/) — this manual assumes you're reading source, modifying behavior, or running infrastructure.

## Conventions

- **Topic-based files** (not numbered chapters). Reference docs are read by jumping to a topic, not front-to-back.
- **Code paths cited as `path/to/file.rs:LINE`** so you can grep / open in your editor.
- **"Shipped" claims include the dev-log entry** (e.g. *M6.11 ships H1 — see `dev-log/127`*) so the chronology is recoverable.
- **Wire-format / schema docs include exact JSON examples** matching what's actually shipped, not synthetic.

## Topics

| File | Subject |
|---|---|
| [`app-architecture.md`](app-architecture.md) | The big picture: three surfaces (GUI / CLI REPL / print) backed by one engine (Agent + Session + ToolRegistry), single-crate Cargo layout + frontend-before-Rust build chain, process model (main thread = tao event loop, worker thread = tokio agent runtime, translator + forwarder threads), `WorkerState` + `ShellInput`/`ViewEvent` channel topology, wry+tao webview + custom `thclaws://` protocol, the Rust↔JS IPC bridge (both directions), filesystem sandbox + `.thclaws/` write-policy + intentional carve-outs, settings layering + key resolution, build-metadata injection, cancellation, dev escape hatches |
| [`agentic-loop.md`](agentic-loop.md) | The agent loop end-to-end: per-turn pipeline (prompt compose → compact → provider stream + retry → assemble → tool dispatch → loop / stop), cancellation model (`CancelToken`), every stop condition, plan-mode integration, approval gate, MCP-Apps widget fetch, truncation-to-disk, wire-format examples |
| [`context-composer.md`](context-composer.md) | What goes into the system prompt and tool definitions: static layer (CLAUDE.md cascade + memory + KMS + skills strategy) + dynamic layer (plan reminder + todos reminder), per-section size caps, token budgeting, the M6.18 system-prompt deduction, single-source-of-truth for "what does the model see right now" |
| [`compaction.md`](compaction.md) | Context-window compaction consolidated: five entry points (`compact` per-turn drop-oldest + min-1 truncation rescue, `compact_with_summary` for `/compact`, `compact_for_step_boundary` / `clear_for_step_boundary` for plan mode, `truncate_oversized_message`), the M6.18 H1 system-prompt + 1 KiB tool-def deduction, JSONL `CompactionEvent` checkpoint persistence + replay, prompt-cache invalidation, `pre_compact` / `post_compact` hooks, subagent / team interactions, gaps (SUB7, TEAM-M5). Cross-jump entry point — detail still lives in `agentic-loop.md` §5, `context-composer.md` §5, `plan-mode.md` §9.3, `sessions.md` §8 |
| [`marketplace.md`](marketplace.md) | Skill / MCP / plugin catalogue: three-layer cache, JSON schema, trust model, deployment, EE policy override |
| [`skills.md`](skills.md) | Skill subsystem: discovery + priority, lazy loading, in-skill scripts (interpreter detection + auto-venv), install dispatch, system-prompt strategies, lifecycle |
| [`commands.md`](commands.md) | Prompt commands (`.md` slash templates): file format, four standard dirs + plugin extras, `$ARGUMENTS` substitution, resolution flow, skills-vs-commands decision criteria |
| [`plugins.md`](plugins.md) | Plugin subsystem: manifest schema, project/user scopes + cross-precedence, install / enable / disable / remove / gc lifecycle (live-refresh vs needs-restart matrix), org-policy URL gate, manifest-name sanitization, MCP trust propagation |
| [`mcp.md`](mcp.md) | MCP client subsystem: stdio + Streamable HTTP, OAuth 2.1 + PKCE, allowlist, qualified-name sanitization, JSON-RPC layer; **MCP-Apps in detail** — trust model, host↔widget postMessage protocol, display modes, approval gate |
| [`kms.md`](kms.md) | Knowledge Management System (llm-wiki pattern): three-layer source/page/schema architecture, YAML frontmatter convention, ingest workflows (file + URL + PDF + source/page split + re-ingest cascade), KmsRead/Search/Write/Append tools (sandbox carve-out), `/kms` slash commands, lint health check, system-prompt injection (categorized index + schema), Obsidian compatibility |
| [`memory.md`](memory.md) | Long-lived agent memory: directory resolution, on-disk layout, frontmatter convention (`name`, `description`, `type`, `category`, `created`, `updated`), system-prompt injection (categorized index + 16 KB body budget + on-demand `MemoryRead`), MemoryRead/Write/Append tools (sandbox carve-out), `/memory` slash commands (list/read/write/append/edit/delete + editor flow), auto-maintained `MEMORY.md` index |
| [`loop-and-goal.md`](loop-and-goal.md) | `/loop` iteration scheduler + `/goal` audit-driven completion: composable Ralph-style overnight builder. `LoopState` + `GoalState` types, broadcaster pattern, `UpdateGoal` model-callable tool, embedded `goal_continue.md` audit-prompt template, rewrite-before-match for `/goal continue`, auto-stop on terminal goal status, CLI vs GUI plumbing |
| [`todo.md`](todo.md) | TodoWrite scratchpad: wire format, `.thclaws/todos.md` markdown layout, M6.30 validation chain (symlink defense + control-char sanitization + server-side status validation + duplicate-id check), per-turn `build_todos_reminder` injection, plan-mode block (M6.20 BUG M1), GUI custom checklist card, sandbox carve-out family (TodoWrite + KmsWrite + MemoryWrite) |
| [`subagent.md`](subagent.md) | Recursive in-process delegation via the `Task` tool: three-tier planning hierarchy (subagent vs team vs TaskCreate), wire format + AgentDef markdown frontmatter, `ProductionAgentFactory` build pipeline (system prompt + tool registry composition, propagation invariants), recursion + depth tracking, CLI vs GUI registration sites + the SUB3 ordering rule, M6.20 BUG H1 approver propagation, M6.33 SUB1–SUB4 audit fixes (GUI registration + disallowed_tools enforcement + tool-filter ordering + cancel propagation) |
| [`side-channel.md`](side-channel.md) | User-driven concurrent subagents via the `/agent <name> <prompt>` slash command: shares AgentDef registry + ProductionAgentFactory with the model-driven Task tool but spawns on its own tokio task, runs concurrently with main, and never enters main's context. `crate::side_channel` module with process-wide registry singleton, `spawn_side_channel` lifecycle, independent `CancelToken::new()` per spawn (main's Cmd-C does NOT propagate), `AgentOrigin::SideChannel { id, agent_name }` end-to-end attribution from `Agent::with_origin` through `ApprovalRequest`/`GuiApprovalRequest` to the React modal, 5 `ViewEvent::SideChannel*` variants with chat dispatch + terminal ANSI render arms, `/agent` / `/agents` / `/agent cancel` slash dispatch, `WorkerState.agent_factory` + `.agent_defs` plumbing, frontend `ApprovalModal` originator tagging + `ChatView` per-id side-channel bubble, `CancelToken::child()` primitive (used by future Subagent depth tagging, not by side channels), test surface (~17 new tests) and known gaps (Subagent depth tagging, persistent threads, parallel fan-out, /agent cancel-all) |
| [`dream.md`](dream.md) | `/dream [FOCUS]` slash command: built-in KMS consolidation via a side-channel agent. Embedded AgentDef compiled into the binary at `default_prompts/dream.md` and seeded into `AgentDefsConfig` at the lowest priority by `seed_builtins`, so user `.thclaws/agents/dream.md` overrides win. New `KmsDelete` tool + `kms::delete_page` helper alongside the existing write/append helpers (path-safety via shared `writable_page_path`, index-bullet pruning, deleted-log entry). `SlashCommand::Dream { focus }` variant; empty focus dispatches a default consolidate-everything prompt; non-empty focus passes through verbatim. GUI-only dispatch reuses `spawn_side_channel` from [`side-channel.md`](side-channel.md). Four-pass operating procedure (survey / read sessions / consolidate / summarize) baked into the system prompt with tight tool whitelist (KMS surface + Read/Glob/Grep + TodoWrite — no Bash/Edit/Write/Memory/Task/Web*). `permission_mode: auto` with git-diff review pattern. Test surface (~7 new tests) covers KmsDelete behaviour, seed-and-override, and `/dream` parsing. Known gaps (10-session window cap, no KmsList tool, in-place edit not "candidate output" mode, no scheduled-dream hookup, GUI-only) |
| [`plan-mode.md`](plan-mode.md) | Sequentially-gated, audit-reviewable execution: four plan tools (`EnterPlanMode` / `ExitPlanMode` / `SubmitPlan` / `UpdatePlanStep`), Layer-1 transition gate + the M6.7 "blocked by upstream" path, Layer-2 narrowed system reminder + per-attempt continuation prompt, dispatch-time block layers (M6.20 BUG M1 TodoWrite block + generic mutating-tool block + approval-window gate), the M6.1 Ralph driver (per-step retry budget + force-Failed + step-boundary compaction), M4.4 + M6.31 PM2 stalled-turn detector, M6.3 cross-step `output` channel + UTF-8 safe truncation, sidebar IPC handlers (Approve / Cancel / Retry / Skip / Stalled-Continue), CLI `/plan` slash, JSONL persistence + session-swap hygiene (M6.31 PM1), plan-completion auto-restore, mutex-poison recovery (M6.31 PM3 + PM4) |
| [`agent-team.md`](agent-team.md) | Multi-process parallel agents via filesystem mailboxes: subprocess-per-teammate (`thclaws --team-agent <name>`), `.thclaws/team/` on-disk layout (config / inboxes / agents / tasks), 10 team tools (`TeamCreate` / `SpawnTeammate` / `SendMessage` / `CheckInbox` / `TeamStatus` / `TeamTaskCreate-List-Claim-Complete` / `TeamMerge`), Mailbox + TaskQueue with `fs2` file locking, lead lifecycle (registration + inbox poller + `handle_team_messages` + EOF kill), teammate lifecycle (subprocess spawn + env vars + system-prompt addendum + inbox loop + idle-notification + shutdown protocol), worktree auto-creation (`.worktrees/<name>` on `team/<name>` branch) + `TeamMerge`, tmux integration, BashTool lead/teammate hard-blocks (`lead_forbidden_command` + `teammate_forbidden_command`) + Write/Edit merge-conflict carve-out (`lead_resolving_merge_conflict`), M6.34 audit fixes (TEAM1 name validation + TEAM2 shell-escape + TEAM3 scoped kill + TEAM4 lead allow-list + TEAM5 send-before-mark + TEAM6 status/list/get locking), `team_grounding_prompt` three-state framing (enabled / agent-sdk-unreachable / disabled), `agent_team.md` + `lead.md` + `worktree.md` embedded prompts |
| [`hooks.md`](hooks.md) | User-defined shell hooks on agent lifecycle events: 8 events (`pre_tool_use` / `post_tool_use` / `post_tool_use_failure` / `permission_denied` / `session_start` / `session_end` / `pre_compact` / `post_compact`) + per-event env vars, `HooksConfig` schema + precedence, `fire()` lifecycle (tokio reaper task → `wait()` with timeout → SIGKILL on expiry, `Stdio::null()` on all three to prevent terminal corruption), per-event call sites in agent / shared_session / repl, subagent inheritance via `ProductionAgentFactory.hooks`, GUI error broadcaster routing to `ViewEvent::SlashOutput`, security model (env inheritance is intentional user trust), M6.35 audit fixes (HOOK1 wire-up of orphaned subsystem + HOOK5 zombie reaper + HOOK6 null stdio + HOOK7 timeout + HOOK8 snake-case event name + HOOK9 byte-cap truncation + HOOK10 GUI chat visibility) |
| [`serve-mode.md`](serve-mode.md) | M6.36 single-user webapp via `thclaws --serve --port <N>`: Axum HTTP server + WebSocket IPC bridge sharing the same `WorkerState` engine the desktop GUI uses, `crate::ipc` transport-agnostic dispatch table + `IpcContext`, `crate::event_render` shared `ViewEvent → JSON` translator, `crate::server` route + per-connection handler, frontend `useIPC.ts` environment detection (wry vs WebSocket) + reconnect-with-backoff + `ws_status` synthetic events, deploy workflow (`cd <project> && thclaws --serve` is the unit; rsync project + drop `.thclaws/.env`; SSH tunnel for remote access), trust model (Phase 1: localhost-only, SSH tunnel handles auth), handler-arm migration state (text-input / approval / cancel / new-session migrated; ~60 wry-only arms still pending), known gaps (snapshot frame for reconnect, file/team/settings/plan/KMS sidebar handlers, Phase 2 auth, project-scoped memory) |
| [`running-modes.md`](running-modes.md) | One engine, four surfaces: GUI (wry+tao desktop) / CLI REPL (`thclaws-cli` or `--cli`) / Headless print (`-p`) / Web (`--serve`). Mode-selection matrix, binary + feature-flag layout (`thclaws` vs `thclaws-cli`, the `gui` feature gate covering `tao`/`wry`/`comrak`/`rfd`/`native-dialog`), per-mode process model + dispatch chain, what's shared (Agent loop, Session JSONL, ToolRegistry, permissions, hooks, memory, KMS, plan, subagent, team) vs what's per-mode, the M6.36 `handle_ipc returns bool` invariant that lets GUI delegate to the shared dispatch then fall through for wry-only arms, configuration loading layering, build matrix (`make build` / `build-cli` / `build-app` / `serve`), mixing modes against the same `.thclaws/` state directory |
| [`schedule.md`](schedule.md) | Recurring agent jobs across three composable layers (manual run / in-process scheduler / native daemon) with three trigger types (cron / manual / `watchWorkspace` filesystem watch). On-disk schema (`~/.config/thclaws/schedules.json`), `run_once_with` single-source-of-truth fire mechanism, `InProcessScheduler` cursor + skip-overlap semantics, daemon lifecycle (PID file + launchd plist + systemd-user unit + SIGTERM/SIGINT handler), `WatchManager` (notify-debouncer-mini + 2s debounce + 60s cooldown + hardcoded ignore segments + reconcile-every-30s pattern), CLI subcommand surface, slash-command surface (`/schedule add` modal vs CLI help-blurb divergence), schedule-add modal IPC protocol (`schedule_add_open` / `schedule_add_submit` / `schedule_cron_preview`), modal helpers (preset chips + live next-fire preview + watchWorkspace checkbox), test surface (24 unit + 6 integration + 8 IPC), Windows status (Steps 1+2 should work, Step 3 stubbed), known gaps (no IPC, no rotation, no `.gitignore`, fixed cooldown) |
| [`line-bridge.md`](line-bridge.md) | LINE OA ↔ thClaws desktop bridge: client + relay protocol, `/pair` + `/unpair` + WS + `/reply/:id` wire shapes, surface-aware tool guidance (AskUserQuestion suppression on LINE-driven turns), `LineGated` permission posture with Quick Reply approvals, profile cache schema (`line_users` Postgres), implementer guidance to prefer LINE Messaging API reply over push (free-tier quota preservation), pointers to the workspace-only official relay (`crates/line-server/`). plan-10 browser-chat addendum: external-browser chat at `chat.thclaws.ai` (magic-link auth, broker channel multiplex, Redis history-replay stream, `LineApprover` routing to browser-modal vs LINE Quick Reply, `chat_logout` + idle TTL + session-expired UX). Required reading for anyone self-hosting a relay |
| [`sso.md`](sso.md) | OIDC sign-in (`crate::sso`): standard `BuiltinProvider::{Google, Azure}` resolved from `*_CLIENT_ID` / `*_CLIENT_SECRET` env vars (+ CI-injected `BUNDLED_*` in official builds) and EE policy-driven override. Module layout (`builtin.rs` + `discovery.rs` + `loopback.rs` + `pkce.rs` + `storage.rs`), state-payload branching, IPC handlers (`sso_status` / `sso_login` / `sso_logout`), the v0.9.5 `secrets::keychain_*_raw` collision fix that lets Dotenv-backend users still sign in, refresh window dance, the gateway-side `verify_id_token` middleware expected by plan-09 (signature + `aud` + `iss` + `exp` against Google's JWKS), threat model (the `aud` check is what makes bundling `GOOGLE_CLIENT_SECRET` in distributed binaries safe). 35 SSO tests pinned |
| [`docker.md`](docker.md) | Container packaging for `thclaws --serve`. Three-stage Dockerfile (Node frontend → Rust builder with GTK/WebKit2GTK dev libs → debian-slim runtime with the matching `-0` runtime libs + git/curl/ripgrep). Image carries GTK + WebKit2GTK at runtime because `--serve` is gated behind the `gui` Cargo feature (`bin/app.rs:308`) and the binary is dynamically linked to those libs even though `server::run` never reaches `gui::run` — followup: split `crate::server` out of the gui feature gate to drop ~250 MB. Why `--bind 0.0.0.0` inside the container with `127.0.0.1:8443:8443` on the host (preserving the localhost-only invariant pinned in `crates/core/src/server.rs:626`). Volume layout (`./:/workspace` for project state, named `thclaws-config` volume for user-level settings; container runs as root by default to make Linux bind-mount writes work without UID juggling). Credential model — no OS keychain in the container, `--env-file` / mounted `.thclaws/.env` are the paths. Tag scheme (`:latest` / `:edge` / pinned `:N.N.N`), multi-arch via `docker buildx`, publish workflow (`docker login -u thclaws -p $DOCKERHUB_ACCESS_TOKEN` from the public mirror). Known limits (no CI auto-publish yet, image bloat from gui feature, single-binary image only) |
| [`paperclip-adapter.md`](paperclip-adapter.md) | External npm package `@thclaws/paperclip-adapter` that lets a Paperclip instance hire a thClaws agent as a built-in runtime. v0.1 MVP wraps `thclaws -p` via `node:child_process`. ServerAdapterModule contract (`type` / `execute` / `testEnvironment` / `models` / `agentConfigurationDoc`), metadata layer (dependency-free entry point so Paperclip's UI can import it without `node:child_process`), spawn pipeline (config parse with defensive `asString`/`asNumber`/`asStringArray`/`asEnvRecord` helpers → optional `{{prompt}}` template → argv `["-p", prompt, "-m", model, ...extraArgs]` → env composition with `PAPERCLIP_*` passthrough → `onMeta` audit log with API keys stripped → spawn → dual stream capture (`onLog` + buffered `stdoutBuf`) → SIGTERM+5s SIGKILL timeout → `AdapterExecutionResult` with `errorFamily: null`), diagnostic probe (`cwd_check` + `thclaws_version` only; no provider auth check by design), token-summary parsing helpers (`extractTokenSummary` / `stripTokenSummary`) for the trailing `[tokens: …]` line, v0.1 limits (no multi-turn resume, no incremental tool-call rendering, no structured `resultJson` — all gated on `thclaws --output-format stream-json` which is declared in `thclaws/crates/core/src/repl.rs:3542` but not wired through `run_print_mode` yet), source layout (workspace-only at `paperclip-adapter/`, excluded from `make sync-public`) |
| [`research.md`](research.md) | `/research <query>` background pipeline that runs entirely outside the agent loop: direct `Provider::stream()` calls + direct tool invocations, spawned via `tokio::spawn` for true backgrounding. `ResearchManager` process-wide singleton with per-job `CancelToken` + wall-clock deadline. Iteration loop with hard floor (`min_iter`) / score threshold / hard ceiling (`max_iter`) — score parser is permissive (decimal, fraction, percent, defaults to 0.0 on failure to keep loop alive). Multi-page synthesis (M6.39.6): `plan_pages` LLM call returns JSON page plan, parallel `write_research_page` per entry via `tokio::spawn`, post-process pipeline (`rewrite_cross_links` → `ensure_sources_section` → `linkify_citations`) makes Obsidian-style `[[wikilinks]]` resolve to actual filenames + canonical numbered-list `## Sources` + clickable `[N](path)` inline citations. KMS write layout (`pages/<run-prefix>__<slug>.md` + `_summary.md` per-run sections + `sources/<url-slug>.md` cached fetches). HAL-first fetch routing (M6.39.8): when `HAL_API_KEY` set, prefer `WebScrape` for clean Markdown, fall back to `WebFetch` on absence/failure. Broadcaster wire-up fires `ViewEvent::ResearchUpdate` per state change + `ViewEvent::KmsUpdate` once per Done transition. CLI completion auto-print (per-process `notified_research: HashSet<JobId>`). Test surface (~108 tests; mock provider + mock tools cover end-to-end without network) |

## When to add a topic

Add a topic file when:
- A subsystem has non-obvious load/refresh/persistence semantics that aren't apparent from reading one source file
- Multiple modules cooperate (e.g. plan-mode spans `plan_state`, `agent`, `shared_session`, `compaction`, `permissions`, `gui`)
- The subsystem has a trust model, deployment workflow, or external-facing schema that operators / contributors need to know
- The subsystem has EE / open-core split behavior

Don't add a topic file for:
- Simple isolated modules (the docstrings + code are enough)
- Active design discussions (those go in [`dev-plan/`](../dev-plan/))
- Sprint records (those go in [`dev-log/`](../dev-log/))
