@jeffober/thatch 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,31 @@
1
+ # UC-006: Cursor hook lifecycle
2
+
3
+ **Preconditions**
4
+ - `thatch setup --cursor` has run; `.cursor/mcp.json` registers the thatch MCP server
5
+ - A store with at least one memory (for the recall tier)
6
+
7
+ **Steps**
8
+ 1. Start a Cursor agent session; the MCP server boots and opens its sideband socket.
9
+ 2. Have the agent run a non-thatch tool (e.g. read a file). The `postToolUse` hook runs
10
+ `thatch buffer-tool`, reading Cursor's stdin (`conversation_id`), and appends one
11
+ interaction to the session's queue file. Stdout is empty.
12
+ 3. Have the agent run a thatch tool (`mcp__thatch__memory_list`). The hook fires but filters
13
+ it out — nothing is appended.
14
+ 4. Submit a prompt. The `beforeSubmitPrompt` hook runs `thatch flush-tools --json`.
15
+ 5. Submit the next prompt with no fresh tool activity since step 4.
16
+
17
+ **Expected**
18
+ - `buffer-tool` normalizes `conversation_id` to a safe session file name and appends exactly one
19
+ interaction per call; thatch's own `mcp__thatch__*` tools never enter the queue (no self-echo).
20
+ - `flush-tools` drains the queue first (extraction tier): prints the JSON payload wrapped for
21
+ Cursor as `additional_context`, and **deletes the queue file** so the next call carries no
22
+ repeat payload unless new tool activity happened.
23
+ - With the queue empty and the sideband socket live, a semantically matching prompt yields a
24
+ recall nudge using the bare `memory_recall` tool name; below `THATCH_RECALL_THRESHOLD`
25
+ (default 0.55), no nudge.
26
+ - `buffer-tool` and `flush-tools --json` output is the shape Cursor's `additional_context`
27
+ field consumes.
28
+
29
+ _Automatable: partial — the `buffer-tool` and `flush-tools --json` CLI contracts are fully
30
+ automatable; the live Cursor agent loop (booting the MCP server, the agent acting on a nudge)
31
+ is manual._
@@ -0,0 +1,31 @@
1
+ # UC-007: Prompt-aware recall nudge
2
+
3
+ **Preconditions**
4
+ - A store with a memorable fact, e.g. "staging DB at staging-db.internal:5432"
5
+ - An opencode session AND (separately) a Claude Code / Cursor session with the MCP server
6
+ running and the sideband socket live
7
+
8
+ **Steps**
9
+
10
+ **opencode path**
11
+ 1. Send a prompt matching the memory, e.g. "where does staging data live?"
12
+ 2. Send an unrelated prompt.
13
+ 3. Send a prompt shorter than 10 characters.
14
+
15
+ **Claude Code / Cursor path**
16
+ 4. With the MCP server running and the sideband socket live, send a matching prompt (queue empty).
17
+ 5. Stop the MCP server; send a matching prompt.
18
+
19
+ **Expected**
20
+ - opencode: the `chat.message` hook embeds the prompt text (skipped when shorter than 10 chars),
21
+ searches `db.search([repo, global])`; a match at or above 0.55 pushes a synthetic nudge part
22
+ with labels and scores (singular wording for one match; up to two labels plus "etc." for more).
23
+ - No match above threshold produces no nudge.
24
+ - The nudge uses `db.search`, not `db.recall`, so **no recall telemetry is stamped** — usage
25
+ tracking fires only on explicit `thatch_memory_recall` / CLI `search`, not on nudges.
26
+ - Claude Code / Cursor: `flush-tools` asks the warm MCP server via the sideband socket; the
27
+ returned labels become a nudge using the bare `memory_recall` name. With the MCP server down,
28
+ the recall nudge is absent (UC-009 covers the write-nudge fallback).
29
+
30
+ _Automatable: partial — the sideband embed + search round-trip and threshold logic are
31
+ unit-tested in `sideband.test.ts`; a live agent observing and acting on the nudge is manual._
@@ -0,0 +1,26 @@
1
+ # UC-008: Hygiene heartbeat
2
+
3
+ **Preconditions**
4
+ - A store with: one memory not updated or recalled in 90+ days; one memory scoped to a git
5
+ branch that no longer exists; two near-duplicate memories (similarity > 0.85)
6
+ - A second, clean store for the negative case
7
+
8
+ **Steps**
9
+ 1. `thatch hygiene` (from the repo)
10
+ 2. `thatch reminder` (Claude Code hook shape) and `thatch reminder --json` (Cursor)
11
+ 3. Start an opencode session and a Claude Code / Cursor session (the session-start path)
12
+ 4. Run the same commands from a directory that is **not** a git repo
13
+
14
+ **Expected**
15
+ - The report names three signals — pending duplicate pairs, stale-memory count, orphaned-branch
16
+ memory count — and **only the non-zero ones**. A fully healthy store prints "Store is healthy."
17
+ (or `null`) and the reminder omits the hygiene block entirely.
18
+ - Both `reminder` shapes fold the hygiene block into the session-start text only when it is non-null.
19
+ - Outside a git repo, the orphaned-branch check is **skipped** — a missing branch list would
20
+ otherwise make everything look orphaned; stale and dup signals still report.
21
+ - Signals are advisory: thatch never deletes or merges memories itself. The agent acts on the
22
+ nudge to fix the store, by the same `memory_forget` / `dedup_mark_checked` tools as UC-002.
23
+
24
+ _Automatable: yes — `thatch hygiene` and `hygieneReport()` are pure functions over the DB and
25
+ git state (`hygiene.test.ts` covers the unit contract); the live session-injection path is
26
+ manual._
@@ -0,0 +1,31 @@
1
+ # UC-009: flush-tools tier priority and sideband degradation
2
+
3
+ **Preconditions**
4
+ - A Claude Code / Cursor session; `.mcp.json` and hooks installed
5
+ - At least one memory for the recall tier to match
6
+ - For the failure modes: a way to stop the MCP server and to leave a stale socket file
7
+
8
+ **Steps**
9
+ 1. Put buffered non-thatch tool interactions in the queue (via `buffer-tool` / `buffer-batch`),
10
+ then run `thatch flush-tools --json`.
11
+ 2. With the queue empty and the MCP server running, send a prompt matching a memory (recall tier),
12
+ then a prompt below threshold.
13
+ 3. Stop the MCP server; run `flush-tools` with an empty queue.
14
+ 4. Leave a stale socket file from a crashed MCP server; run `flush-tools`.
15
+ 5. Make the sideband server hang (> 2 s); run `flush-tools`.
16
+
17
+ **Expected**
18
+ - Tiers fire in strict priority — **extraction > recall > write** — at most one nudge per call:
19
+ - extraction: queue non-empty -> JSON payload, queue file deleted.
20
+ - recall: queue empty, socket live, match at or above `THATCH_RECALL_THRESHOLD` (default 0.55)
21
+ -> nudge with match labels; below threshold -> no nudge.
22
+ - write: socket unavailable, no matches, or socket error -> static
23
+ "did you learn anything worth persisting?" nudge.
24
+ - Sideband failure **never blocks** the agent: server down, a stale socket file, and a > 2 s
25
+ timeout all degrade to the write nudge. A stale socket file left by a crash is cleaned up on
26
+ connection error.
27
+ - `--json` wraps whichever nudge fires for Cursor's `additional_context`; plain stdout for Claude Code.
28
+
29
+ _Automatable: yes — `flush-tools` is a deterministic CLI contract and sideband failure modes
30
+ (server down, stale socket, timeout) plus tier ordering are filesystem/IPC only, with no LLM in
31
+ the loop._
@@ -0,0 +1,25 @@
1
+ # UC-010: thatch prime
2
+
3
+ **Preconditions**
4
+ - Thatch installed and `bun` on PATH
5
+ - At least one of: opencode (for `opencode run`), Cursor (`agent` CLI), or Claude Code
6
+ (`claude`) on PATH
7
+
8
+ **Steps**
9
+ 1. From a project root, run `thatch prime`
10
+ 2. Let the selected child CLI run the `thatch-project-primer` skill to completion
11
+ 3. After it finishes, inspect the project store (`thatch list`)
12
+
13
+ **Expected**
14
+ - `prime` resolves the first available CLI in order — `opencode run`, then
15
+ `agent -p --approve-mcps` (Cursor), then `claude` — and spawns it with the project-primer
16
+ prompt.
17
+ - The primer skill recalls existing memories first, investigates the codebase from multiple
18
+ angles, and writes foundational memories via `thatch_memory_remember` (project vs global
19
+ assignment per its own rules), then runs dedup afterward.
20
+ - `prime` exits with the **child process's exit code** (0 on success).
21
+ - If no supported CLI is on PATH, `prime` fails fast with a clear error rather than spawning
22
+ nothing.
23
+
24
+ _Automatable: partial — the CLI dispatch selection (which child, which flags, exit-code
25
+ pass-through) is automatable; the LLM's real investigation and memory writing are manual._
@@ -0,0 +1,24 @@
1
+ # UC-011: Write-time similarity warning
2
+
3
+ **Preconditions**
4
+ - A store with a memory, e.g. `API rate limit` -> "the API rate limit is 100 req/min"
5
+ - A second, similar memory not yet saved
6
+
7
+ **Steps**
8
+ 1. Save the second memory under a different label with similar content, without
9
+ `overwrite: true`.
10
+ 2. Read the tool's response.
11
+
12
+ **Expected**
13
+ - The save **succeeds** (`[saved] ...`) — the warning never blocks. The response
14
+ also carries a warning listing the existing memory with a similarity score and
15
+ instructions to reconcile.
16
+ - Two reconciliation paths both work:
17
+ - Merge: `memory_remember` the merged content with `overwrite: true` on the
18
+ better label, then `memory_forget` the other. The survivor has the combined
19
+ content; the deleted label is gone.
20
+ - Mark distinct: `dedup_mark_checked` the pair as `unrelated`. A subsequent
21
+ `find_duplicates` no longer reports the pair.
22
+
23
+ _Automatable: yes — the "save proceeds + warning lists similar entries" contract
24
+ and both reconciliation paths are pure tool/DB calls with no LLM in the loop._
@@ -0,0 +1,28 @@
1
+ # UC-012: THATCH_MODEL migration (mixed embedding dimensions)
2
+
3
+ **Preconditions**
4
+ - A store with memories embedded by a model of dimension A (e.g. the default
5
+ 384-dim `bge-small-en-v1.5`)
6
+ - A second model available whose vectors have a **different** dimension
7
+
8
+ **Steps**
9
+ 1. `export THATCH_MODEL=<other-model>` (different vector dimension).
10
+ 2. Start a fresh session. `memory_list` and `memory_show` an old memory by label.
11
+ 3. `memory_recall` for a topic that matches an old memory.
12
+ 4. Save a new memory with the new model; `memory_recall` for it.
13
+ 5. `memory_show` an old memory and inspect its `model` tag.
14
+
15
+ **Expected**
16
+ - `list` and `show` still return the old memories — the data is intact, **not
17
+ corrupted and not deleted**.
18
+ - `recall` returns **no matches** for the old memories: entries whose vector
19
+ dimension differs from the query are skipped, not scored. This is a silent
20
+ skip, not an error.
21
+ - The new memory embeds at the new dimension and is recallable.
22
+ - `show` reveals the old memories carry the old model tag while new ones carry
23
+ the new tag. There is **no automatic re-embedding** — old memories stay
24
+ invisible to search until re-saved.
25
+
26
+ _Automatable: yes — dimension-skip and data-intact assertions are pure DB calls;
27
+ use two `MockEmbeddingModel` instances with different dimensions (no network, no
28
+ real model download)._
@@ -0,0 +1,25 @@
1
+ # UC-013: Compaction context (opencode-only)
2
+
3
+ **Preconditions**
4
+ - An opencode session that grows long enough to trigger context compaction
5
+ - Several memories already stored in the repo's store
6
+
7
+ **Steps**
8
+ 1. Work in the session until opencode compacts the context window.
9
+ 2. Inspect the compaction output.
10
+ 3. After compaction, ask the agent something that should use a memory tool.
11
+
12
+ **Expected**
13
+ - The compaction output includes the thatch re-familiarization context block
14
+ pushed by `experimental.session.compacting` — it reminds the agent that
15
+ `thatch_memory_recall` exists and that memories persist.
16
+ - After compaction, the agent still knows the memory tools exist and uses them
17
+ (it does not "forget" thatch just because the window was summarized).
18
+ - This behavior is **opencode-only**. Claude Code's `PostCompact` hook is
19
+ side-effects only and cannot inject context; Cursor has no equivalent hook.
20
+ See `docs/dev/mcp-parity.md` for the gap and its mitigation (CLAUDE.md /
21
+ AGENTS.md persists through compaction; the `SessionStart` recall reminder
22
+ runs again after compaction in Claude Code).
23
+
24
+ _Automatable: no — requires a live opencode compaction event. The hook's string
25
+ content is unit-tested, but actual compaction injection is manual._
@@ -0,0 +1,28 @@
1
+ # UC-014: Skill install and drift recovery
2
+
3
+ **Preconditions**
4
+ - Thatch installed; the host's skills directory empty
5
+ - `bun` on PATH
6
+
7
+ **Steps**
8
+ 1. `thatch setup --claude` (and/or start an opencode session, and/or
9
+ `thatch setup --cursor`).
10
+ 2. List the skills directory.
11
+ 3. Edit one `SKILL.md` file locally to introduce drift.
12
+ 4. Re-run `setup` (or restart opencode).
13
+ 5. Check whether the `thatch-code-review` coordinator skill is present.
14
+
15
+ **Expected**
16
+ - Claude Code and Cursor install exactly **10 shared skills** to the skills
17
+ dir — the coordinator (`thatch-code-review`) is **absent** (it needs
18
+ sub-agents, which those hosts lack).
19
+ - opencode installs **11** — the 10 shared plus the coordinator.
20
+ - The locally edited `SKILL.md` is **overwritten** with the canonical content
21
+ on the next `setup`/init (drift detection: a file is only rewritten when its
22
+ content differs from the definition). Unrelated skill files are untouched.
23
+ - Skills never land in the worktree — always under the user-scoped config dir
24
+ (`~/.claude/skills`, `~/.cursor/skills`, `$XDG_CONFIG_HOME/opencode/skills`).
25
+
26
+ _Automatable: yes — file presence, count (10 vs 11), content-diff-overwrite, and
27
+ coordinator host-gating are all file assertions (`setup.test.ts` already covers
28
+ the unit contract)._
@@ -0,0 +1,28 @@
1
+ # UC-015: Environment variable override matrix
2
+
3
+ **Preconditions**
4
+ - Writable temp directories; `bun` on PATH
5
+
6
+ **Steps**
7
+ Set each variable and confirm the effect at the documented resolution point.
8
+
9
+ | Variable | Set to | Expected effect |
10
+ |----------|--------|------------------|
11
+ | `THATCH_DB_PATH` | a temp file path | the SQLite DB is created there (CLI + plugin + MCP) |
12
+ | `XDG_CONFIG_HOME` | a temp dir | the default DB (`$XDG_CONFIG_HOME/thatch/thatch.db`) and opencode skill dir move there; the real `~/.config` is untouched |
13
+ | `CLAUDE_CONFIG_DIR` | a temp dir | `thatch setup --claude --global` writes CLAUDE.md, settings.json, and skills under that dir; project-local keeps project paths but skills under the custom dir |
14
+ | `THATCH_QUEUE_DIR` | a temp dir | extraction-queue JSONL files land there instead of `$XDG_CACHE_HOME/thatch/queue` |
15
+ | `THATCH_RECALL_THRESHOLD` | a low value (e.g. `0.2`) | the prompt-aware recall nudge fires for weaker matches (a prompt that matched nothing at 0.55 now surfaces results) |
16
+ | `THATCH_MODEL` | a different model name | new memories carry the new model tag and vector dimension; old memories become invisible to search (see UC-012) |
17
+
18
+ **Expected**
19
+ - Each override takes effect at the point the docs describe (see
20
+ `docs/dev/setup-and-hooks.md` and the dev README config section). None require
21
+ a restart beyond the process that reads them.
22
+ - `THATCH_DB_PATH` and `XDG_CONFIG_HOME` change the **same** sideband socket
23
+ path (it is a hash of the resolved DB path) — so a hook process and the MCP
24
+ server must both resolve the same DB path or they miss each other.
25
+
26
+ _Automatable: yes — env resolution is deterministic and side-effect-free;
27
+ `setup.test.ts` already covers `CLAUDE_CONFIG_DIR`; the rest follow the same
28
+ shape._
@@ -0,0 +1,36 @@
1
+ # UC-016: Concurrent session isolation
2
+
3
+ **Preconditions**
4
+ - Two opencode sessions in the same repo (A and B)
5
+ - (Separately) two Claude Code / Cursor hook processes with different session IDs
6
+
7
+ **Steps**
8
+
9
+ **opencode (in-memory buffer)**
10
+ 1. In session A, run a non-thatch tool (the extraction ring buffer records it).
11
+ 2. In session B, send a message — `chat.message` runs.
12
+ 3. In session A, send a message — `chat.message` runs.
13
+
14
+ **Claude Code / Cursor (file-backed queue)**
15
+ 4. Hook process for session A calls `buffer-tool`/`buffer-batch`.
16
+ 5. Hook process for session B calls `buffer-tool`/`buffer-batch`.
17
+ 6. `flush-tools` is invoked for session A, then for session B.
18
+
19
+ **Sideband (shared warm model)**
20
+ 7. Both sessions' `flush-tools` connect to the same sideband socket.
21
+
22
+ **Expected**
23
+ - **No cross-session bleed** in either path: session B's `chat.message` produces
24
+ no nudge from session A's buffered interaction; session B's message only sees
25
+ its own buffer. The in-memory `ExtractionPipeline` and the file-backed queue are
26
+ both keyed by session ID.
27
+ - `flush-tools` for session A drains only A's queue file and deletes it; a second
28
+ flush for A is empty. Session B's queue file is untouched and drains on its own
29
+ flush.
30
+ - The sideband socket is **shared** (one warm MCP server) but **stateless per
31
+ query**: each request embeds a fresh prompt and returns its own matches. Two
32
+ concurrent match requests do not interfere.
33
+
34
+ _Automatable: yes — `extraction.test.ts` (in-memory per-session) and
35
+ `extract-queue.test.ts` (file-backed per-session) already cover isolation; the
36
+ sideband is a stateless request/response so concurrency is trivially safe._
@@ -0,0 +1,27 @@
1
+ # UC-017: buffer-tool vs buffer-batch contract
2
+
3
+ **Preconditions**
4
+ - A Claude Code setup (`PostToolBatch` -> `buffer-batch`) and a Cursor setup
5
+ (`postToolUse` -> `buffer-tool`)
6
+
7
+ **Steps**
8
+ 1. **Claude Code**: a `PostToolBatch` fires with a `tool_calls` array (several
9
+ tools, one a `mcp__thatch__*` call) and a `session_id`.
10
+ 2. **Cursor**: a `postToolUse` fires for a single tool with a `conversation_id`.
11
+ 3. Run `flush-tools` for both.
12
+
13
+ **Expected**
14
+ - `buffer-batch` (Claude Code) reads the `tool_calls` array and `session_id`,
15
+ appends all interactions to the session's JSONL queue, filters out
16
+ `mcp__thatch__*` calls, and caps at 20 (oldest dropped). It is **silent**.
17
+ - `buffer-tool` (Cursor) reads **one** tool call, normalizes `conversation_id`
18
+ to a safe filename (unsafe chars -> underscore), appends that single
19
+ interaction, filters `mcp__thatch__*`, same 20 cap. Also silent.
20
+ - Both write the same `ToolInteraction` shape to the same JSONL format, so
21
+ `flush-tools` drains them identically and `buildExtractionPayload` produces an
22
+ identical contract for the fact-extractor skill.
23
+ - Thatch's own tools never appear in either queue (no self-echo feedback loop).
24
+
25
+ _Automatable: yes — `extract-queue.test.ts` covers both shapes (batch with
26
+ `tool_calls` and single-tool), the `mcp__thatch__*` filter, the 20 cap, and
27
+ back-compat with `buildExtractionPayload`._
@@ -1,10 +1,14 @@
1
1
  # Thatch
2
2
 
3
- Persistent memory for opencode. Thatch gives your AI agent the ability to
4
- remember information across sessions using local embeddings and SQLite.
3
+ Persistent memory for AI coding agents. Thatch gives your agent the ability to
4
+ remember information across sessions using local embeddings and SQLite. It
5
+ works with **OpenCode** (as a plugin), **Claude Code** (as an MCP server), and
6
+ **Cursor** (as an MCP server).
5
7
 
6
8
  ## Installation
7
9
 
10
+ ### OpenCode
11
+
8
12
  Publish to npm and add thatch to your opencode config:
9
13
 
10
14
  ```jsonc
@@ -24,6 +28,36 @@ For local development before publishing, use a file path:
24
28
 
25
29
  Or place the thatch repo in `.opencode/plugins/` for auto-loading.
26
30
 
31
+ ### Claude Code
32
+
33
+ Install the MCP server, hooks, instructions, and skills into Claude Code:
34
+
35
+ ```bash
36
+ thatch setup --claude # project-local (writes .mcp.json, CLAUDE.md, .claude/)
37
+ thatch setup --claude --global # user-scoped (~/.claude/)
38
+ ```
39
+
40
+ `bun` must be on PATH (the thatch binary runs under bun). `setup` is idempotent
41
+ — re-running it updates drifted content without clobbering unrelated config.
42
+ For a global install, `setup` prints the `claude mcp add --scope user` command
43
+ to run instead of writing a project `.mcp.json`.
44
+
45
+ ### Cursor
46
+
47
+ ```bash
48
+ thatch setup --cursor # project-local (.cursor/mcp.json, AGENTS.md, .cursor/hooks.json)
49
+ thatch setup --cursor --global # user-scoped (~/.cursor/)
50
+ ```
51
+
52
+ Cursor has no `claude mcp add` equivalent — `setup` writes `~/.cursor/mcp.json`
53
+ directly. Hook output is JSON-wrapped (`--json`) so Cursor injects it as
54
+ `additional_context`.
55
+
56
+ ### Before running setup
57
+
58
+ The `thatch` binary must resolve `bun` on PATH. Install from npm (`npm i -g
59
+ @jeffober/thatch`) or from a checkout (`bun run bin/thatch`).
60
+
27
61
  ## How it works
28
62
 
29
63
  ### Stores
@@ -84,8 +118,9 @@ Beyond the tools, thatch hooks into opencode itself:
84
118
 
85
119
  On startup thatch installs [skills] into your global opencode config
86
120
  (`~/.config/opencode/skills/`, or `$XDG_CONFIG_HOME/opencode/skills`).
87
- When using `thatch setup --claude`, skills are installed to `~/.claude/skills/`
88
- (or `$CLAUDE_CONFIG_DIR/skills/`).
121
+ With `thatch setup --claude`, skills install to `~/.claude/skills/` (or
122
+ `$CLAUDE_CONFIG_DIR/skills/`); with `thatch setup --cursor`, to
123
+ `~/.cursor/skills/`.
89
124
 
90
125
  ### Memory skills
91
126
 
@@ -129,13 +164,13 @@ Five specialist review lenses, each a self-contained static-analysis pass:
129
164
  For a **quick single-lens review**, load any specialist skill directly and
130
165
  point it at a branch or commit range:
131
166
 
132
- ```
167
+ ```text
133
168
  Load thatch-review-pedantic and review the changes on this branch.
134
169
  ```
135
170
 
136
171
  For a **full multi-specialist review on opencode**, load the coordinator:
137
172
 
138
- ```
173
+ ```text
139
174
  Load thatch-code-review and review branch feature-x.
140
175
  ```
141
176
 
@@ -145,7 +180,7 @@ specialist lens), and synthesize a final report.
145
180
  For a **full review on Claude Code** (or without the coordinator), run each
146
181
  specialist in sequence, then synthesize:
147
182
 
148
- ```
183
+ ```text
149
184
  1. Load thatch-review-pedantic, review branch feature-x, report findings.
150
185
  2. Load thatch-review-acceptance, review the same branch.
151
186
  3. ... repeat for state-flow, no-slop, breadcrumbs ...
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.5",
4
- "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin) and Claude Code (as an MCP server).",
3
+ "version": "0.1.6",
4
+ "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
7
7
  "types": "src/index.ts",
@@ -28,6 +28,7 @@
28
28
  "devDependencies": {
29
29
  "@opencode-ai/plugin": "^1.17.13",
30
30
  "@types/bun": "latest",
31
+ "markdownlint-cli2": "^0.23.0",
31
32
  "typescript": "^5.7.0"
32
33
  },
33
34
  "peerDependencies": {
package/src/db.ts CHANGED
@@ -234,9 +234,12 @@ export class ThatchDB {
234
234
  /**
235
235
  * Brute-force cosine similarity search across the given stores.
236
236
  * When branch is specified, includes project-wide (branch IS NULL) plus
237
- * branch-specific entries.
237
+ * branch-specific entries. Does NOT stamp recall telemetry — the prompt-aware
238
+ * nudge uses this to check whether memories relate to a prompt without
239
+ * polluting recall_count/last_recalled_at (the agent hasn't actually read
240
+ * them yet, only the plugin has checked for relevance).
238
241
  */
239
- recall(
242
+ search(
240
243
  stores: string[],
241
244
  queryEmbedding: Float32Array,
242
245
  opts?: { branch?: string; limit?: number },
@@ -281,10 +284,21 @@ export class ThatchDB {
281
284
  });
282
285
 
283
286
  scored.sort((a, b) => b._score - a._score);
284
- const top = scored.slice(0, limit);
287
+ return scored.slice(0, limit);
288
+ }
289
+
290
+ /**
291
+ * Semantic recall for agent-initiated searches. Delegates to search() for
292
+ * cosine scoring, then stamps recall telemetry — retrieval is the "used
293
+ * recently" signal hygiene reporting keys on.
294
+ */
295
+ recall(
296
+ stores: string[],
297
+ queryEmbedding: Float32Array,
298
+ opts?: { branch?: string; limit?: number },
299
+ ): (MemoryRow & { _score: number })[] {
300
+ const top = this.search(stores, queryEmbedding, opts);
285
301
 
286
- // Recall telemetry: retrieval is the "used recently" signal hygiene
287
- // reporting keys on, so returned rows get stamped here.
288
302
  if (top.length > 0) {
289
303
  const now = new Date().toISOString();
290
304
  const rowKeys = top.map(() => "(?, ?)").join(", ");
package/src/git.ts CHANGED
@@ -54,7 +54,7 @@ const URL_PATTERNS: UrlPattern[] = [
54
54
  * 3. Fall back to the directory basename
55
55
  */
56
56
  export async function detectRepo(cwd?: string): Promise<string> {
57
- const dir = cwd ?? process.cwd();
57
+ const dir = cwd ?? process.env.CURSOR_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
58
58
 
59
59
  try {
60
60
  const remote = await $`git remote get-url origin`.cwd(dir).quiet();
package/src/index.ts CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  systemPrompt,
9
9
  compactionContext,
10
10
  sessionStartReminder,
11
+ recallNudge,
12
+ type NudgeMatch,
11
13
  } from "./prompts";
12
14
  import { ExtractionPipeline } from "./extraction";
13
15
  import { installSkills, SHARED_SKILLS, OPENCODE_ONLY_SKILLS } from "./skills";
@@ -17,6 +19,15 @@ import { hygieneReport } from "./hygiene";
17
19
  // V1 server export — tools, prompt injection, session hooks
18
20
  // ---------------------------------------------------------------------------
19
21
 
22
+ // Minimum cosine score for the prompt-aware recall nudge. Lower than
23
+ // findDuplicates' 0.85 (near-dupes) because "relates to" is a weaker signal
24
+ // than "duplicate." Tunable via THATCH_RECALL_THRESHOLD.
25
+ const RECALL_THRESHOLD = parseFloat(process.env.THATCH_RECALL_THRESHOLD ?? "0.55");
26
+
27
+ // Prompts shorter than this skip the recall nudge — trivially short prompts
28
+ // like "yes" or "ok" match too broadly to be useful.
29
+ const MIN_PROMPT_LEN = 10;
30
+
20
31
  export const server: Plugin = async ({ client, worktree }) => {
21
32
  // The opencode server's cwd is wherever the server happened to start;
22
33
  // `worktree` is the project this plugin instance actually serves.
@@ -72,27 +83,62 @@ export const server: Plugin = async ({ client, worktree }) => {
72
83
  });
73
84
  },
74
85
 
75
- // 4. Per-message nudge — when interactions are queued, hand the agent the
76
- // actual payload; the thatch-fact-extractor skill's contract is "you will
77
- // be given a JSON payload", so the nudge must carry it.
86
+ // 4. Per-message nudge — two priority tiers:
87
+ // a. Extraction nudge: prior tool interactions are queued for fact
88
+ // extraction (carries the JSON payload for thatch-fact-extractor).
89
+ // b. Recall nudge: the user's prompt semantically matches existing
90
+ // memories — surface that prior knowledge exists before the agent
91
+ // responds. Uses the in-process warm model (no sideband needed).
78
92
  "chat.message": async (input, output) => {
79
- if (!extraction.pending(input.sessionID)) return;
80
-
81
- const batch = extraction.flush(input.sessionID);
82
- const payload = extraction.buildPayload(batch, repo);
83
- const text =
84
- `[thatch] ${batch.length} recent tool interactions are queued for fact extraction. ` +
85
- `Use the skill tool to load thatch-fact-extractor, then use thatch_memory_remember ` +
86
- `to save any new durable facts from this payload:\n${payload}`;
93
+ if (extraction.pending(input.sessionID)) {
94
+ const batch = extraction.flush(input.sessionID);
95
+ const payload = extraction.buildPayload(batch, repo);
96
+ const text =
97
+ `[thatch] ${batch.length} recent tool interactions are queued for fact extraction. ` +
98
+ `Use the skill tool to load thatch-fact-extractor, then use thatch_memory_remember ` +
99
+ `to save any new durable facts from this payload:\n${payload}`;
100
+
101
+ output.parts.push({
102
+ id: `prt_thatch_${Math.random().toString(36).slice(2)}`,
103
+ sessionID: input.sessionID,
104
+ messageID: input.messageID ?? output.message.id,
105
+ type: "text",
106
+ text,
107
+ synthetic: true,
108
+ });
109
+ return;
110
+ }
87
111
 
88
- output.parts.push({
89
- id: `prt_thatch_${Math.random().toString(36).slice(2)}`,
90
- sessionID: input.sessionID,
91
- messageID: input.messageID ?? output.message.id,
92
- type: "text",
93
- text,
94
- synthetic: true,
95
- });
112
+ // No extraction pending — try the prompt-aware recall nudge. Extract
113
+ // the user's prompt text from the message parts, embed it with the
114
+ // warm in-process model, and search for matches. Best-effort: any
115
+ // failure (no text, model not loaded, empty store) silently skips.
116
+ try {
117
+ const promptText = (output.parts as any[])
118
+ .filter((p) => p.type === "text" && !p.synthetic)
119
+ .map((p) => p.text)
120
+ .join(" ");
121
+ if (promptText.length < MIN_PROMPT_LEN) return;
122
+
123
+ const embedding = await model.queryEmbed(promptText);
124
+ const results = db.search([repo, "global"], embedding, { limit: 5 });
125
+ const matches: NudgeMatch[] = results
126
+ .filter((r) => r._score >= RECALL_THRESHOLD)
127
+ .map((r) => ({ label: r.label, score: Math.round(r._score * 1000) / 1000 }));
128
+
129
+ if (matches.length === 0) return;
130
+
131
+ output.parts.push({
132
+ id: `prt_thatch_${Math.random().toString(36).slice(2)}`,
133
+ sessionID: input.sessionID,
134
+ messageID: input.messageID ?? output.message.id,
135
+ type: "text",
136
+ text: recallNudge(matches),
137
+ synthetic: true,
138
+ });
139
+ } catch (err) {
140
+ console.error(`[thatch] recall nudge failed: ${err}`);
141
+ }
96
142
  },
97
143
 
98
144
  // 5. Session-start reminder, carrying the hygiene heartbeat. Hygiene is