-
Notifications
You must be signed in to change notification settings - Fork 1
Developer Guide
Feature docs describe behavior in opencode v1 client.* terms; the opencode v2 equivalents and degrades are catalogued in features/opencode-plugin.md.
Thatch has three integration paths sharing a common core:
- OpenCode plugin — runs inside opencode's Bun runtime. Full access to plugin hooks: system prompt injection, session events, tool buffering, compaction context.
- Claude Code MCP server — runs as a stdio JSON-RPC process. Tools exposed via MCP; session behavior driven by Claude Code hooks.
-
Cursor MCP server — same stdio MCP server as Claude Code; session
behavior driven by Cursor hooks in a flat
hooks.jsonformat.
For feature parity and gaps across the three, see mcp-parity.md. For the concrete files and hook events each host writes, see setup-and-hooks.md. For non-obvious invariants, see gotchas.md. For the skill system, see skills.md.
Shared core
├── tool-defs.ts → single source of truth: zod schemas + execute logic
├── db.ts → SQLite CRUD, cosine search (recall + search), dedup verdicts
├── embeddings.ts → embedding model via transformers.js
├── git.ts → detect repo identity (store name) + worktree-deletion recovery (repo_paths cache)
├── hygiene.ts → hygiene report (pending dedups, stale, orphaned branches)
├── prompts.ts → system prompt, compaction, reminders, recall/prediction/behavior nudges, prompt cores, CLAUDE.md instructions
├── commands.ts → /thatch slash-command registry: actions from prompt cores + wrap-ups, per-host sync
├── sideband.ts → Unix socket server + client: warm-model semantic match for hook processes
├── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
├── scoring-engine.ts → generic four-table scoring base (shared by prediction + behavior engines)
├── prediction.ts → prediction engine wrapper (thin layer over ScoringEngine)
├── behavior.ts → behavior engine wrapper (thin layer over ScoringEngine)
├── seed-behaviors.ts → default behavior seeding on first run
├── session-db.ts → read-only opencode.db access: session timeline, full parts,
│ decoded-content search, OpenAI transcript builder (backs the
│ `thatch session` CLI and the opencode-only session tools)
└── vector-math.ts → blobToVector + cosineSimilarity helpers
OpenCode plugin path
├── index.ts → dual-shape plugin entry: merged default export
│ ({id, setup, server}) loads on opencode v1 AND v2;
│ lazy-imports the adapter matching the host
├── opencode/v1.ts → v1 adapter: @opencode-ai/plugin hooks object
├── opencode/v2.ts → v2 adapter: @opencode/plugin promise-context domains
├── capabilities.ts → HostCapabilities seam: the host operations the
│ runtime needs; capabilitiesFromClient bridges the
│ v1 SDK client
├── runtime.ts → shared plugin runtime: nudges, system prompt
│ injection, session events, extraction triggers,
│ child-session bookkeeping (host-agnostic)
├── os-args.ts → pure argv/OS-command-line helpers (startup session
│ resolution; SDK-free)
├── tools.ts → thin opencode tool() wrappers (v1); tool-defs.ts
│ owns buildCoreContext (shared per-call context)
├── extraction.ts → in-memory ring buffer + shared payload builders
├── watchers.ts → in-memory watcher registry + poller (opencode-only,
│ event-driven PR notifications delivered as prompts)
└── chat.ts → cross-session chat: shared SQLite directory + inbox
(ChatStore) and the per-process wake-up poller
(ChatPoller, opencode-only)
MCP server path
├── mcp.ts → stdio JSON-RPC server: z.toJSONSchema() for tools/list,
│ z.object().parse() for validation, dispatches to tool-defs;
│ opens sideband socket for warm-model match queries
├── sideband.ts → Unix socket: SidebandServer (embed + search via warm model)
│ and sidebandMatch (thin client for hook processes)
├── extract-queue.ts → file-backed JSONL queue (Claude Code + Cursor hooks)
└── setup.ts → `thatch setup --claude` / `--cursor`: writes .mcp.json,
CLAUDE.md / AGENTS.md, settings/hooks JSON, installs skills
bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygiene|
prime|buffer-batch|buffer-tool|flush-tools|flush-predictions|setup|
session (archaeology)|chat (list|tail)
| Module | Responsibility |
|---|---|
tool-defs.ts |
Single source of truth for all tools. Each tool has a name, description, zod schema (args), and execute function. Framework-agnostic — neither opencode nor MCP specific. |
tools.ts |
Thin opencode wrappers. buildCoreContext constructs the shared per-call context; createTools wraps each tool-def in opencode's tool() with a thatch_ prefix (v1 only). |
mcp.ts |
Stdio JSON-RPC 2.0 server. Compiles zod schemas to JSON Schema via z.toJSONSchema() for tools/list. Validates args via z.object().parse() in tools/call. All logging to stderr (stdout is the transport). |
index.ts |
Dual-shape OpenCode plugin entry: the merged default export ({ id, setup, server }) loads on opencode v1 (reads default.server) and v2 (reads default.setup, strips excess keys). Lazy-imports the host adapter so each host's SDK resolves only under its own runtime. |
opencode/v1.ts + opencode/v2.ts
|
Host adapters. v1 builds HostCapabilities from the PluginInput client and returns the hooks object; v2 registers through the promise-context domains (ToolEditor, session.hook, event.subscribe) and degrades the surfaces v2 lacks. |
capabilities.ts |
The HostCapabilities seam: the host operations the shared runtime needs (session create/prompt/get/list/messages/delete, toast, compaction trigger, app exit). capabilitiesFromClient bridges the v1 SDK client. |
runtime.ts |
Shared plugin runtime: wires DB, model, extraction; the nudge tiers, system prompt injection, session event handling, child-session bookkeeping, wrap-up resolution. Host-agnostic: consumed by both adapters. |
os-args.ts |
Pure argv / OS-command-line helpers for startup session resolution (-s/-c). SDK-free. |
setup.ts |
thatch setup --claude / --cursor installer. Writes MCP config (.mcp.json / .cursor/mcp.json), appends to CLAUDE.md / AGENTS.md (idempotent), installs hooks in settings.json / hooks.json, installs skills. |
hygiene.ts |
Hygiene report: pending dedup pairs, stale count, orphaned branch memories. Shared by the plugin's session-start hook and the CLI's thatch reminder command. |
git.ts |
Parse owner/repo from git remote. Worktree-safe fallback chain, plus recovery from the repo_paths cache when the directory is deleted, and the spawn-cwd fallback. See features/repo-identity.md. |
db.ts |
SQLite schema, CRUD for entries/stores, brute-force cosine search (search = pure scoring, recall = search + telemetry stamping), dedup-pair verdict tracking. Prediction tables: matchers, predictions, edges, provenance. scorePredictionNudge is the shared auto-fire entry point for both host paths. Behavior tables: same four-table shape (matchers, behaviors, edges, provenance) with scoreBehaviorNudge as the shared entry point. |
embeddings.ts |
Lazy-load the embedding model. Expose queryEmbed/passageEmbed and the model name (stored as an informational tag). MockEmbeddingModel for tests. |
extraction.ts |
Per-session in-memory ring buffer (cap 20) that buffers non-thatch tool interactions and serializes them into the JSON payload the get_extraction_payload tool returns. The in-memory pipeline is opencode-only, but the payload builders (buildExtractionPayload, deriveTitle) are shared by both paths — extract-queue.ts imports deriveTitle, mcp.ts imports buildExtractionPayload for the extraction payload provider. summarizeArgs is used internally by buildExtractionPayload. |
extract-queue.ts |
File-backed per-session JSONL queue (caps 20, oldest dropped). Shared by the Claude Code and Cursor hook paths, which fire one-shot per event with no cross-call state. This is the MCP-side equivalent of extraction.ts. |
config.ts |
User config file (~/.config/thatch/config.json, beside the DB): Zod section schemas (strict — unknown keys fail validation), load with missing/invalid fallback to defaults, field-level merge, atomic save (temp + rename). Backs the config_get/config_set tools. |
notify.ts |
Out-of-band notification dispatch: darwin (osascript banner + /usr/bin/say voice), linux (notify-send + spd-say/espeak), other platforms report unsupported. Injectable spawner (CoreContext.spawner) so tests never fire real commands. Backs the notify_user tool and the alert state machine. |
alerts.ts |
LLM alert state machine (opencode-only): pause/done/error decisions from session events, per-session dedup (no timer-based debounce - one alert per busy->idle transition, per request id for pauses). Fed by runtime.ts's event hook; delivers through notify.ts. Plain memory by design (a reload loses at most one transition). See ../plans/llm-alerts.md. |
session-db.ts |
Read-only access to the opencode session database (opencode.db): session timeline (session list), full part/message retrieval (session get), OpenAI chat-completions transcript builder (session transcript), and decoded-content search (session search). Backs the thatch session CLI subcommands and the opencode-only session_search/session_get tools. Path resolution honors OPENCODE_DB, then the XDG default; the db opens read-only so a thatch bug cannot corrupt host history. |
watchers.ts |
In-memory watcher registry and background poller for event-driven notifications (opencode-only). Three sources: GitHub PRs and branches via gh api (injected GhRunner), and local shell commands via bash -c (injected CommandRunner) run as condition variables - only the exit code is read, never the command's output. Diffs against last-seen state, queues events (pointer data plus machine status like check conclusions and exit codes, never external content) per session, and delivers them through a plugin-supplied callback when the session is idle. The pollSeconds getter surfaces the poll cadence to tool output and notifications. Process-lifetime state by design - no SQLite. |
chat.ts |
Cross-session chat between opencode sessions on one machine (opencode-only). ChatStore holds the shared directory and inbox in SQLite so any process can read/write them; ChatPoller heartbeats hosted sessions and delivers wake prompts through an injected callback when the recipient is idle - only the recipient's host process delivers, so there is no cross-process double-delivery. Heartbeat staleness flags crashed sessions; a per-recipient nudge rate cap is the anti-loop hard brake. See features/cross-session-chat.md. |
chat-names.ts |
The static display-name pool for the chat directory: whimsical geek-culture names in fnord's Nomenclater style, baked in so name assignment never costs a model call. The pool is the fallback base for registrations with no usable session title. |
sideband.ts |
Unix domain socket server + client. The MCP server (long-lived, warm model) runs SidebandServer so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Handles three methods: match (recall nudge), predictions (prediction auto-fire), and behaviors (behavior auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
prompts.ts |
Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (recallNudge / claudeRecallNudge), prediction nudge (predictionNudge), behavior nudge (behaviorNudge), prediction verb selection (predictionVerb), watcher notification (watcherNotificationNudge), version warning (versionWarningNudge), chat wake notification (chatNotificationNudge), chat transcript echo text and echo recognizer (chatEchoText / isChatEchoParts), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. Also the prompt cores (defragCore, hygieneCore, reflectCore, extractionCore) - host-agnostic behavior instruction bodies shared by nudges and /thatch commands, parameterized by a ToolNamer for per-host tool spellings. See features/commands.md. |
commands.ts |
Slash-command registry and sync. actionDefs(tool) renders the on-demand actions (defrag, extract, hygiene, reflect) from the prompt cores; the wrap-up commands (compact, exit) are bespoke greenlight-gated templates. Renderers produce full command files for opencode (installOpencodeCommands, synced by the plugin at init) and Claude Code (installClaudeCommands, synced by setup); compilePrompts in mcp.ts exposes the same actions as MCP prompts for Cursor. A parity-guard test pins the three sets to the documented exclusions. See features/commands.md. |
skills.ts |
SKILL.md content for all thatch skills, plus the installer. Skills are split into SHARED_SKILLS (fact-extractor, dedup-classifier, project-primer, the review specialists, review synthesizer, review context, code archaeology, review followup, review response, change walkthrough, code walkthrough, session reflection, coding-workflow, plan-refinement, thatch-pr-description, thatch-ticket-description, thatch-split-overlarge-pr, memory-verify, knowledge-export — work on all three hosts) and OPENCODE_ONLY_SKILLS (code-review coordinator — requires sub-agent support, not installed for Claude Code or Cursor). installSkills(dir, skills) defaults to SHARED_SKILLS; the opencode plugin passes [...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]. |
scoring-engine.ts |
Generic four-table scoring engine with Bayesian confidence. Shared base for prediction and behavior engines — each wraps it with table-specific names. |
prediction.ts |
Thin wrapper around ScoringEngine with prediction-specific table names. |
behavior.ts |
Thin wrapper around ScoringEngine with behavior-specific table names. |
seed-behaviors.ts |
Default behavior seeding on first run. Populates starter self-discipline rules. |
vector-math.ts |
blobToVector and cosineSimilarity helpers used across the codebase. |
debug.ts |
Opt-in diagnostic logging (THATCH_DEBUG) to debug.log beside the database, never to the host's stdout/stderr (invisible inside the TUI; would leak into the quality gate from tests). Lines are <iso> [feature:aspect] msg; the env var is 1/all/* for everything or a comma-separated list of tag filters matching a whole tag or its feature part. Add a tagged line wherever a live harness might need to answer "what did the plugin actually see here?" - the first consumer is chat:startup, which records the resolved -s session and every registration guard. |
The v1 adapter (src/opencode/v1.ts) registers these opencode integration
points; the v2 adapter routes its domain hooks into the same shared runtime
methods. The v1 hook shapes:
| Hook | What it does |
|---|---|
experimental.chat.system.transform |
Appends the thatch system prompt (store names, usage rules). |
experimental.session.compacting |
Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
command.execute.before |
Arms the wrap-up greenlight check: /thatch/compact and /thatch/exit mark their session in pendingWrapUp. The next session.status idle resolves it by inspecting the final assistant message for the greenlight token and, when present, triggering the TUI action (session_compact via executeCommand, app.exit via publish; the exit path unregisters the session from the chat directory first). See features/session-lifecycle.md. |
experimental.compaction.autocontinue |
Clears the compacting flag so chat.message nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. The chat.message hook also clears the flag if it fires for a session still in the compacting set but the incoming message has no compaction-type part — this handles compaction failure, where the session would otherwise be stuck with nudges off forever. |
tool.execute.after |
Buffers every non-thatch_*, non-skill, non-task tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (thatch_memory_remember) and thatch_extraction_done drain the buffer and reset the missed-nudge counter. For child sessions (childToParent.has(sessionID)), also tracks metrics: remember with overwrite:false → new++, overwrite:true → updated++, forget → deleted++. For thatch_chat_* tool calls, builds the transcript echo (chatEchoText) and posts it as a visible non-synthetic noReply part. This is a plugin hook, NOT a bus event — do not move it into the event handler; the event bus has no such event and it will silently never fire. |
chat.message |
Recall nudge: embeds the user's prompt text with the in-process warm model, searches db.search() across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire (db.scorePredictionNudge, injects [thatch] User decision model) and the behavior auto-fire (db.scoreBehaviorNudge, injects [thatch] Situational behaviors). All three nudges fire independently in separate try/catch blocks with separate synthetic parts. There is deliberately NO extraction nudge here - extraction is plugin-driven at session idle (see the extraction feature doc); the old model-driven handshake raced its own state machine and was removed. Chat transcript echoes (non-synthetic noReply parts whose text is entirely [chat]-prefixed bubbles) are skipped before any tier — no model turn reads them, so nudging them wastes an embedding. Skipped entirely while the session is compacting (tool calls are blocked during summary generation), and for task-dispatched sub-agent sessions (childToParent without extractionChildren) - their tool lists may exclude the thatch tools and their work is driven by the dispatch prompt. |
event |
Subscribes to all session bus events. session.created: records childToParent + parentSnapshots (shallow copy of the parent's buffer for snapshot-aware drain), then sends the session-start reminder via client.session.prompt carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. session.status: records the session's live status for the watcher and chat delivery gates; on idle, if the session is a child, drains its snapshot, fires a toast with childMetrics, and deletes the child session; if the session is a parent with pending tool interactions and not already extracting, calls triggerExtraction to create a direct-extraction child, then flushes pending watcher events and pending chat mail (the session just became deliverable). session.error: requeues the parent's buffer (child died without draining). session.deleted: cleans up childToParent, parentSnapshots, childMetrics, and extracting, cancels the session's watchers, and unregisters it from the chat directory (the crash path — a dead process fires no event — is covered by heartbeat staleness). session.compacted: clears the compacting flag so chat.message nudges resume. |
dispose |
Stops the version checker, the watcher poller, and the chat poller, closes the DB. |
Hook failures are logged with a [thatch] prefix — never swallowed silently.
Two of these hooks were dead for weeks because failures were invisible.
- No global mutable state. Every module accepts its dependencies explicitly. The plugin entry wires real defaults; tests inject mocks.
-
Embedding is a separate concern.
db.tsknows nothing about embedding models — it stores/retrieves BLOBs and compares vectors handed to it. - Extraction and dedup are agent-driven. The plugin never writes memories on its own. It buffers, nudges, and surfaces candidates; the agent does the writing through the ordinary tools (guided by the installed skills). There is deliberately no background classification or locking machinery.
-
Embedding spaces are discriminated by vector dimension, not model tag.
recall/findDuplicatesskip vectors whose length differs from the query. Themodelcolumn is informational. SwitchingTHATCH_MODELmakes old memories invisible to search (not corrupted) until re-embedded. -
Store creation is implicit. First
rememberto a new store creates it. - Default recall scope is repo + global. The tool layer hardcodes this.
-
Skills are plugin-owned files. Installed to
$XDG_CONFIG_HOME/opencode/skills(opencode); for MCP hosts, scope-dependent — the repo's.claude/skills/or.cursor/skills/for project-localthatch setup,~/.claude/skills/or~/.cursor/skills/for--global. Drifted content is overwritten on plugin init or re-runningthatch setup. Skills are split intoSHARED_SKILLS(work on all three hosts) andOPENCODE_ONLY_SKILLS(require sub-agent support, not installed for Claude Code or Cursor). The opencode plugin installs both arrays;thatch setup --claudeand--cursorinstall only shared. -
Tool definitions are the single source of truth.
tool-defs.tsdefines each tool once (name, zod schema, execute function). The opencode plugin wraps them intool()with athatch_prefix; the MCP server wraps them inz.object()for validation andz.toJSONSchema()for the protocol. Adding a tool means adding one entry toTOOL_DEFS. -
Proactive save is prompt-instructed, not hook-driven. All three system
prompts include a "Before Responding" section instructing the agent to
check for durable knowledge (via
thatch_memory_recallfor dedup, thenthatch_memory_remember) before composing a final response after substantial work. No plugin hook fires between generation and response delivery, so the only viable path is prompt instruction. If the agent forgets to save, extraction still sweeps the buffered tool interactions at the session's next idle (plugin-driven; there is no model-facing extraction nudge any more). This is an experiment — model reliability on meta-instructions is uncertain. -
Background completion narration is suppressed, not prevented. When a
background sub-agent completes, opencode injects a
<task_result>block into the parent session and triggers a full model generation. Thatch cannot cancel this turn (no pre-response hook). The mitigations: the fact-extractor skill's return value is a fixed string so the injected block has nothing to narrate AND the string self-identifies as a completion, not an extraction nudge (a bare "Extraction complete." was misread mid-loop as new nudge traffic and re-dispatched - the September 2026 re-dispatch loop); the system prompt's "Background Task Completions" section instructs the model not to narrate completions or treat them as approval to act, and that a completion is never a re-dispatch signal. -
Prediction engine is a statistical model, not an LLM call. The query
(embed prompt, cosine-match against matchers, score linked predictions)
is mechanical — same shape as
thatch_memory_recallbut against different tables. The agent drives formation and evaluation via tools (thatch_prediction_updatewith confirm/disconfirm/soft/create signals) guided by system prompt instructions. No wall-clock decay; confidence is relevance-gated (being tested moves it, not being ignored). See the prediction DB tables, auto-fire inchat.message, and the sidebandpredictionsmethod for the MCP path. -
Behavior engine mirrors the prediction engine but is self-graded. Same
four-table data model, same Bayesian confidence, same auto-fire pipeline.
The difference: predictions model what the USER wants (graded by user
feedback); behaviors model what the LLM should do (graded by the LLM's own
ham/spam relevance judgment via
behavior_feedback). The firewall principle from predictions (the model that uses predictions cannot grade them) does not apply: the LLM grading its own behavioral rules is the point, not a violation. The ham/spam is a relevance judgment ("does this rule apply here?"), not a value judgment ("is this a good rule?"). An anti-laziness guard in the prompt prevents the agent from codifying shortcuts. See the behavior DB tables, auto-fire inchat.message, and the sidebandbehaviorsmethod for the MCP path.
thatch_memory_remember(label?, content)
→ model.passageEmbed("# label\n\ncontent") → Float32Array
→ db.findSimilar(store, embedding) — write-time collision check (no telemetry)
→ db.remember(store, label, content, embedding, model.name, opts)
overwrite:false → atomic INSERT (PK constraint rejects duplicates)
overwrite:true → upsert + clear stale dedup verdicts for that slug
→ confirmation string, plus a ⚠ warning naming ≥0.85-similar existing
memories — the save always proceeds; the agent decides how to reconcile
thatch_memory_recall(query)
→ model.queryEmbed(query) → Float32Array
→ db.recall([repo, "global"], queryEmbedding, {branch?, limit})
skips entries with mismatched embedding dimension
cosine similarity, sort desc, top-N
stamps recall_count/last_recalled_at on returned rows (usage telemetry)
→ formatted results with scores
dedup cycle (agent-driven)
→ thatch_find_duplicates → pairs above threshold, minus checked pairs,
grouped into clusters (connected components; presentation-only)
→ agent loads thatch-dedup-classifier skill; classifies pairs, consolidates
clusters of 3+ into one memory
→ merges/deletes via thatch_memory_remember(overwrite)/thatch_memory_forget
→ thatch_dedup_mark_checked records verdicts for surviving pairs
(overwriting or forgetting an entry clears its verdicts → can re-flag)
extraction cycle
→ tool.execute.after buffers non-thatch, non-skill, non-task tool calls
per session (max 20); for child sessions, also tracks new/updated/deleted
metrics
→ direct extraction (the only opencode path):
parent goes idle (session.status idle) with pending tool interactions
→ stale accepted entries requeue (15-min bound), then triggerExtraction
adds parentID to the `extracting` set, creates a child session via
client.session.create, and prompts it with extractionDirectPrompt
(the parent's session ID is interpolated by the plugin)
→ child fetches the payload via thatch_get_extraction_payload - the
fetch records its claim (the delivery record)
→ child runs the fact-extractor skill, writes memories via
thatch_memory_remember - each write (or extraction_done) completes
the child's claimed delivery, consuming only what it received
→ child goes idle → event handler finalizes, fires a toast with
childMetrics, deletes the child session
→ failure recovery (no model-facing nudge exists): a child error or
deletion requeues what it held to pending; a completion signal that
never comes is bounded by the 15-minute stale reaper; either way the
next idle re-triggers extraction from the same pending buffer
→ MCP path (Claude Code/Cursor): unchanged — no SDK client, no child
sessions; extract-queue.ts + flush-tools drives the nudge via hooks
toast notifications (opencode-only, TUI-rendered)
→ client.tui.showToast — best-effort, silently ignored if TUI not
connected (headless mode); model-invisible (goes to the user only,
not the conversation history — the inverse of synthetic nudge parts,
which are model-visible but TUI-hidden)
→ extraction metrics: `[thatch] new: N, updated: M, deleted: K` (success
variant, 4s) — fires when an extraction child goes idle; no-save runs
show `[thatch] extraction complete — nothing to save` (info variant)
→ recall matches: `[thatch] recalled N memories` (info variant, 3s) —
fires when chat.message matches stored memories
→ prediction matches: `[thatch] N predictions surfaced` (info variant,
3s) — fires when chat.message matches decision-model patterns
→ behavior matches: `[thatch] N behaviors surfaced` (info variant,
3s) — fires when chat.message matches codified behavior matchers
prompt-aware recall nudge (both paths)
→ opencode: chat.message hook embeds prompt text with warm in-process model,
searches db.search([repo, global]), pushes nudge part if matches ≥ threshold
→ Claude Code/Cursor: flush-tools connects to MCP server's sideband socket,
warm server embeds + searches, returns labels; hook prints nudge or falls
back to write nudge if socket unavailable or no matches
→ threshold: THATCH_RECALL_THRESHOLD env (default 0.55) — lower than
findDuplicates' 0.85 because "relates to" is a weaker signal than "duplicate"
→ no telemetry stamped: uses db.search(), not db.recall()
hygiene heartbeat (session start)
→ hygieneReport(db, repo, worktree): pending dedup pairs; entries neither
updated nor recalled in 90+ days; memories scoped to branches that no
longer exist (skipped when worktree isn't a git repo)
→ non-zero signals appended to the session-start reminder; the agent tends
the store when convenient — the plugin never deletes memories itself
prediction cycle (agent-driven, statistical model)
→ formation: agent calls thatch_prediction_update(matcher, prediction, signal)
with signal = create|confirm|disconfirm|soft. The tool embeds the matcher
text (raw, no header prepend — unlike memory_remember), finds or creates
a matcher (0.85 dedup), finds or creates a prediction (0.85 store-wide
dedup), links them via an edge, and adjusts confidence via a Bayesian
posterior: (confirm + K*P0) / (confirm + disconfirm + K), K=5, P0=0.5.
Soft signals count as 0.25 of a full signal. No wall-clock decay;
confidence is relevance-gated (being tested moves it, not being ignored).
→ auto-fire (opencode): chat.message reuses the prompt embedding already
computed for the recall nudge. db.scorePredictionNudge([repo, global],
embedding, 0.60) finds matchers above threshold, follows edges to
predictions, scores by cosine * weight * confidence, dedups by
prediction_id, and returns top 5. Injects a separate synthetic part
with a [thatch] User decision model block. 0-evidence predictions use
"you may prefer"; predictions with evidence use "you tend to".
→ auto-fire (Claude Code/Cursor): flush-tools fires the prediction query
via the sideband socket's `predictions` method in parallel with the
recall nudge. Same scorePredictionNudge entry point prevents scoring
drift between host paths.
→ consumption: agent follows strong predictions silently, surfaces
ambiguous/competing predictions to the user, and calls
thatch_prediction_update to reinforce or weaken after the user responds.
thatch_prediction_list inspects the model with provenance;
thatch_prediction_delete removes bad predictions (cascade clears edges
and provenance).
behavior cycle (agent-driven, self-graded)
→ formation: agent calls thatch_behavior_codify(situation, behavior, rationale)
when it recognizes a situation it should react to in a specific, repeatable
way. The tool embeds the situation text, finds or creates a behavior matcher
(0.85 dedup), finds or creates a behavior (0.85 store-wide dedup), links
them via an edge. Confidence starts at p0 (0.5) with 0 evidence.
→ auto-fire (opencode): chat.message reuses the prompt embedding. db.
scoreBehaviorNudge([repo, global], embedding, 0.60) finds behavior matchers
above threshold, follows edges to behaviors, scores by cosine * weight *
confidence, dedups by behavior_id, returns top 5. Injects a separate
synthetic part with a [thatch] Situational behaviors block. 0-evidence
behaviors use "consider"; behaviors with evidence use "do".
→ auto-fire (Claude Code/Cursor): flush-tools fires the behavior query via
the sideband socket's `behaviors` method in parallel with recall and
predictions. Same scoreBehaviorNudge entry point prevents scoring drift.
→ consumption: agent evaluates each surfaced behavior against the current
situation. If relevant (ham), follows it and calls behavior_feedback with
relevant: true (confirm). If not relevant (spam), calls behavior_feedback
with relevant: false (disconfirm). The feedback adjusts the Bayesian
confidence the same way prediction signals do. thatch_behavior_list
inspects with provenance; thatch_behavior_delete removes bad rules
(cascade clears edges and provenance).
- Single SQLite file at
$XDG_CONFIG_HOME/thatch/thatch.db(default~/.config/thatch/thatch.db), WAL mode, 5s busy timeout. - Tables:
stores(name PK),entries(slug, store, label, content, embedding BLOB, model, branch, confidence, archived, created_at, updated_at, recall_count, last_recalled_at, PK(slug, store)),dedup_pairs(store, slug_a, slug_b, status, checked_at, PK(store, slug_a, slug_b)),prediction_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at),predictions(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at),prediction_edges(matcher_id, prediction_id, weight REAL, PK(matcher_id, prediction_id), FK CASCADE),prediction_provenance(id PK, prediction_id, signal, detail, created_at, FK CASCADE),behavior_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at),behaviors(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at),behavior_edges(matcher_id, behavior_id, weight REAL, PK(matcher_id, behavior_id), FK CASCADE),behavior_provenance(id PK, behavior_id, signal, detail, created_at, FK CASCADE),chat_sessions(session_id PK, name UNIQUE COLLATE NOCASE, topic, project, host_kind, registered_at, last_seen, auto, worktree),chat_messages(id PK AUTOINCREMENT, from_session, to_session, body, created_at, delivered_at, read_at, via_broadcast),chat_name_counters(base PK, next)- per-base counters for assigned chat names; only ever increments.chat_leave_tombstones(session_id PK, left_at)- explicit leaves; suppresses auto-registration until chat_register rejoins. -
recall_count,last_recalled_at, andarchivedare added to pre-existing databases by an idempotent column migration at init (PRAGMA table_info+ALTER TABLE). Thearchivedcolumn isINTEGER NOT NULL DEFAULT 0(0 = live, 1 = archived); search, dedup, and staleness queries all exclude archived entries by default. A second init migration rebuilds a pre-NOCASEchat_sessionstable to enforce case-insensitive name uniqueness (see features/database.md). - Embeddings are raw Float32Array bytes. Serialization honors
byteOffset/byteLength— transformers.js can return views into larger tensor buffers, and serializing the whole backing buffer corrupts vectors. - Slugs: lowercase, whitespace→
-, unicode letters/digits preserved, hash fallback for all-symbol labels. ASCII slugs match earlier releases.
- Default model:
Xenova/bge-small-en-v1.5(384-dim), override withTHATCH_MODEL. - First load downloads ~34 MB from Hugging Face Hub; cached thereafter. Load is lazy (first embed call) and memoized against concurrent init.
- Query prefix:
"Represent this sentence for searching relevant passages: "; passages get no prefix (BGE asymmetric-search convention). - All embedding computation is local — no API calls.
bun install # deps
bun test # full suite; no network, no real config dirs
mise run check # typecheck + bun test + markdownlint (the CI gate)
opencode # self-host via opencode.json plugin path (see root README)The markdownlint gate lints README.md and docs/ (excluding the historical
docs/plans/) via .markdownlint-cli2.jsonc. It disables three rules that
conflict with intentional house style (line length, table alignment, and the
use-case template's bold-label + tight-list format). Run mise run lint-md
alone to check docs without the test suite. Run mise run typecheck for
tsc alone (uses tsconfig.check.json, which includes the test files that
the build's tsconfig.json excludes).
To watch what a live harness's plugin actually sees at init, launch opencode
with THATCH_DEBUG=chat (or =1 for every tag) and tail
~/.config/thatch/debug.log. The env var must be set on the opencode
process itself: the plugin runs in a worker thread that inherits the
process environment but not its argv.
mise run release patch|minor|majorbin/release runs the full flow:
- Runs
bun test(aborts on failure — no broken releases). -
npm version <bump> --no-git-tag-versionbumpspackage.json. - Prompts to commit, tag (
v<next>), and push; declining revertspackage.jsonand exits cleanly. - On confirm: commits, tags
v<next>, pushes commits and the tag.
Publishing is tag-driven and passwordless. Pushing a v* tag triggers
.github/workflows/publish.yml, which publishes to npm via OIDC trusted
publishing — no stored npm token. The id-token: write permission lets npm
authenticate through GitHub's OIDC exchange instead. Prerequisites:
- A Trusted Publisher configured on npmjs.com for
@jeffober/thatchpointing at thesysread/thatchrepo and this workflow file. - npm >= 11.5.1 (the OIDC exchange needs it).
publish.ymlinstallssetup-node@v4(node 24) andnpm install -g npm@11— npm 12 shipped a sigstore bug that broke OIDC publishing; the pin prevents it. The workflow deliberately sets noregistry-url, which would write a token-expecting.npmrcthat preempts the OIDC exchange.
CI (.github/workflows/ci.yml) runs tsc (typecheck), bun test, and
markdownlint-cli2 on every push/PR to main — the never-merge-broken
guard before a release.
User
- Guide: Behavior Engine
- Guide: Cli
- Guide: Code Review
- Guide: Commands
- Guide: Cross Session Chat
- Guide: Deduplication
- Guide: Default Behaviors
- Guide: Extraction
- Guide: Hygiene
- Guide: Memory
- Guide: Notifications
- Guide: Prediction Engine
- Guide: Overview
- Guide: Setup
- Guide: Skills
- Guide: Watchers
Developer
Dev Feature Guides
- Feature: Behavior Engine
- Feature: Cicd
- Feature: Cli
- Feature: Commands
- Feature: Compaction Recovery
- Feature: Cross Session Chat
- Feature: Database
- Feature: Deduplication
- Feature: Extraction
- Feature: Hygiene
- Feature: Memory Store
- Feature: Multi Host
- Feature: Notifications
- Feature: Nudge Pipeline
- Feature: Opencode Plugin
- Feature: Prediction Engine
- Feature: Qa System
- Feature: Overview
- Feature: Repo Identity
- Feature: Session Lifecycle
- Feature: Setup
- Feature: Sideband
- Feature: Watchers