@jeffober/thatch 0.1.27 → 0.1.29

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.
Files changed (62) hide show
  1. package/README.md +24 -10
  2. package/artifacts/skills/thatch-code-review.md +9 -7
  3. package/artifacts/skills/thatch-fact-extractor.md +2 -2
  4. package/artifacts/skills/thatch-review-economy.md +97 -0
  5. package/artifacts/skills/thatch-review-followup.md +1 -1
  6. package/artifacts/skills/thatch-review-synthesizer.md +15 -5
  7. package/bin/qa-run +38 -0
  8. package/bin/thatch +13 -7
  9. package/docs/dev/README.md +76 -22
  10. package/docs/dev/features/README.md +33 -0
  11. package/docs/dev/features/behavior-engine.md +271 -0
  12. package/docs/dev/features/cicd.md +89 -0
  13. package/docs/dev/features/cli.md +81 -0
  14. package/docs/dev/features/compaction-recovery.md +48 -0
  15. package/docs/dev/features/database.md +244 -0
  16. package/docs/dev/features/deduplication.md +102 -0
  17. package/docs/dev/features/extraction.md +253 -0
  18. package/docs/dev/features/hygiene.md +81 -0
  19. package/docs/dev/features/memory-store.md +154 -0
  20. package/docs/dev/features/multi-host.md +228 -0
  21. package/docs/dev/features/nudge-pipeline.md +282 -0
  22. package/docs/dev/features/prediction-engine.md +253 -0
  23. package/docs/dev/features/qa-system.md +205 -0
  24. package/docs/dev/features/session-lifecycle.md +110 -0
  25. package/docs/dev/features/setup.md +199 -0
  26. package/docs/dev/features/sideband.md +104 -0
  27. package/docs/dev/gotchas.md +3 -3
  28. package/docs/dev/mcp-parity.md +7 -5
  29. package/docs/dev/setup-and-hooks.md +4 -4
  30. package/docs/dev/skills.md +10 -8
  31. package/docs/plans/README.md +2 -2
  32. package/docs/user/README.md +58 -20
  33. package/docs/user/behavior-engine.md +85 -0
  34. package/docs/user/cli.md +98 -0
  35. package/docs/user/code-review.md +73 -0
  36. package/docs/user/deduplication.md +70 -0
  37. package/docs/user/default-behaviors.md +75 -0
  38. package/docs/user/extraction.md +74 -0
  39. package/docs/user/hygiene.md +76 -0
  40. package/docs/user/memory.md +81 -0
  41. package/docs/user/prediction-engine.md +87 -0
  42. package/docs/user/setup.md +110 -0
  43. package/docs/user/skills.md +97 -0
  44. package/package.json +2 -2
  45. package/src/behavior.ts +120 -0
  46. package/src/db.ts +160 -417
  47. package/src/embeddings.ts +0 -41
  48. package/src/extraction.ts +2 -2
  49. package/src/git.ts +3 -1
  50. package/src/index.ts +66 -14
  51. package/src/mcp.ts +59 -2
  52. package/src/prediction.ts +127 -0
  53. package/src/prompts.ts +155 -200
  54. package/src/scoring-engine.ts +374 -0
  55. package/src/seed-behaviors.ts +339 -0
  56. package/src/setup.ts +2 -6
  57. package/src/sideband.ts +36 -8
  58. package/src/skills.ts +1 -0
  59. package/src/tool-defs.ts +235 -9
  60. package/src/tools.ts +11 -1
  61. package/src/vector-math.ts +30 -0
  62. package/docs/qa/README.md +0 -175
@@ -0,0 +1,81 @@
1
+ # CLI (bin/thatch)
2
+
3
+ The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, MCP server startup, hook commands for MCP hosts, and the setup installer.
4
+
5
+ ## Subcommands
6
+
7
+ 15 subcommands:
8
+
9
+ | Command | Args | Flags | Stdin | Purpose |
10
+ |---------|------|-------|-------|---------|
11
+ | `stores` | none | none | none | List all store names |
12
+ | `list [store]` | optional store | none | none | List memory labels in a store |
13
+ | `show <label> [store]` | label, optional store | none | none | Display one memory by label |
14
+ | `forget <label> [store]` | label, optional store | none | none | Delete one memory by label |
15
+ | `search <query> [store]` | query, optional store | none | none | Semantic cosine search (limit 10; "all" searches project + global) |
16
+ | `mcp` | none | none | none | Start the stdio MCP server (for Claude Code, Cursor) |
17
+ | `reminder [--json]` | none | `--json` | none | Print session-start reminder + hygiene report |
18
+ | `hygiene` | none | none | none | Print the hygiene report standalone |
19
+ | `prime` | none | none | none | Run thatch-project-primer skill via opencode/agent/claude CLI |
20
+ | `buffer-batch` | none | none | JSON | Append PostToolBatch payload to queue (Claude Code hook) |
21
+ | `buffer-tool` | none | none | JSON | Append single postToolUse interaction to queue (Cursor hook) |
22
+ | `flush-tools [--json]` | none | `--json` | JSON | Peek queue + extraction/recall/prediction/behavior/write nudge |
23
+ | `flush-predictions [--json]` | none | `--json` | JSON | Standalone prediction-only nudge |
24
+ | `setup --claude [--cursor] [--global]` | none | `--claude`, `--cursor`, `--global` | none | Install config + instructions + hooks + skills |
25
+ | (unknown) | none | none | none | Print usage, exit 1 |
26
+
27
+ ## Global behavior
28
+
29
+ - No `--version`, `--help`, `-h`, or short flags. No subcommand aliases.
30
+ - Unknown command or missing required arg calls `usage()` and exits 1.
31
+ - Args are positional, parsed by hand from `process.argv.slice(2)`. No arg-parsing library.
32
+ - DB opened once at startup, closed at end (except `mcp` which closes early).
33
+ - Stores default to git remote detected by `detectRepo()`; "unknown" on failure. "global" is the shared store. "all" (search only) means project + global.
34
+
35
+ ## prime
36
+
37
+ Runs the thatch-project-primer skill via an external CLI. Searches `PATH` in order: `opencode`, `agent` (Cursor CLI), `claude`. Uses the first found:
38
+ - opencode: `opencode run "<primer prompt>"`
39
+ - agent: `agent -p "<primer prompt>" --approve-mcps`
40
+ - claude: `claude "<primer prompt>"`
41
+
42
+ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none found.
43
+
44
+ ## Environment variables
45
+
46
+ | Variable | Purpose | Default |
47
+ |----------|---------|---------|
48
+ | `XDG_CONFIG_HOME` | Config home base | `~/.config` |
49
+ | `THATCH_DB_PATH` | SQLite DB file location | `$XDG_CONFIG_HOME/thatch/thatch.db` |
50
+ | `THATCH_MODEL` | Embedding model override | `Xenova/bge-small-en-v1.5` |
51
+ | `THATCH_RECALL_THRESHOLD` | Cosine cutoff for recall nudge | `0.55` |
52
+ | `THATCH_PREDICTION_THRESHOLD` | Cutoff for prediction auto-fire | `0.60` |
53
+ | `THATCH_BEHAVIOR_THRESHOLD` | Cutoff for behavior auto-fire | `0.60` |
54
+ | `CLAUDE_PROJECT_DIR` | Claude Code project dir | `process.cwd()` |
55
+ | `CURSOR_PROJECT_DIR` | Cursor project dir | falls through |
56
+ | `CLAUDE_CONFIG_DIR` | Claude config dir | `~/.claude` |
57
+ | `CURSOR_CONFIG_DIR` | Cursor config dir | `~/.cursor` |
58
+ | `OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS` | Enable async extraction | unset |
59
+
60
+ ## Source files
61
+
62
+ - `bin/thatch` — the CLI (Bun script, 486 lines)
63
+ - `bin/release` — release helper (bash, separate from the main CLI)
64
+
65
+ ## Interactions with other features
66
+
67
+ - Memory store ([memory-store.md](memory-store.md)): `stores`, `list`, `show`, `forget`, `search` subcommands
68
+ - Setup ([setup.md](setup.md)): `setup` subcommand
69
+ - Nudge pipeline ([nudge-pipeline.md](nudge-pipeline.md)): `flush-tools`, `flush-predictions`, `reminder` subcommands
70
+ - Extraction ([extraction.md](extraction.md)): `buffer-batch`, `buffer-tool`, `flush-tools` subcommands
71
+ - Hygiene ([hygiene.md](hygiene.md)): `hygiene`, `reminder` subcommands
72
+ - Multi-host ([multi-host.md](multi-host.md)): `mcp` subcommand starts the MCP server for Claude Code and Cursor
73
+
74
+ ## Key invariants
75
+
76
+ - No arg-parsing library. Args are positional, parsed by hand from `process.argv.slice(2)`.
77
+ - No `--version`, `--help`, `-h`, or short flags. No subcommand aliases.
78
+ - DB opened once at startup, closed at end (except `mcp` which closes early).
79
+ - Store name auto-detected from git remote. "global" is the shared store. "all" (search only) means project + global.
80
+ - Unknown command or missing required arg calls `usage()` and exits 1.
81
+ - `prime` searches PATH in order: `opencode`, `agent`, `claude`. Uses the first found.
@@ -0,0 +1,48 @@
1
+ # Compaction Recovery (opencode)
2
+
3
+ opencode compacts long sessions by generating a summary and replacing the conversation history. Without intervention, a compacted session would lose awareness of thatch -- the agent would stop using memory tools, extraction would stop, and nudges would fire during summary generation where tools are blocked.
4
+
5
+ ## What it does
6
+
7
+ - Suppresses nudges during compaction (tools are blocked during summary generation)
8
+ - Injects compaction context so the agent retains awareness of thatch after compaction
9
+ - Clears the compacting flag on success (three mechanisms, belt-and-suspenders)
10
+ - Handles compaction failure (clears stale flag if autocontinue never fired)
11
+
12
+ ## How it works
13
+
14
+ ### Four hooks/events
15
+
16
+ 1. **experimental.session.compacting** (hook) -- fires when opencode starts compacting. Adds sessionID to the `compacting` set. Pushes `compactionContext(repo)` into `output.context`. The context tells the agent to preserve what's been learned and decided this session so there's continuity after compaction.
17
+
18
+ 2. **experimental.compaction.autocontinue** (hook) -- fires after compaction succeeds and the auto-continue turn fires. Removes sessionID from the `compacting` set so nudges resume.
19
+
20
+ 3. **session.compacted** (event) -- fires on successful compaction. Clears the `compacting` flag. Belt-and-suspenders: if autocontinue didn't fire or wasn't installed, the event still clears the flag.
21
+
22
+ 4. **Compaction failure fallback** (chat.message hook) -- if a non-compaction chat.message arrives while the session is in the `compacting` set, compaction failed (autocontinue never fired). The hook clears the stale `compacting` flag and proceeds normally. Without this, the session would be stuck with nudges off forever.
23
+
24
+ ### Why nudges are suppressed during compaction
25
+
26
+ During summary generation, tools are blocked. A nudge that instructs the agent to use `thatch_memory_recall` or `thatch_memory_remember` would cause a blocked-tool error. The `compacting` set is checked at the top of the chat.message hook -- if the session is compacting, all nudges are skipped.
27
+
28
+ ### Compaction context
29
+
30
+ `compactionContext(repo)` in `src/prompts.ts` generates text that tells the agent: you are using thatch, here's what you've learned and decided this session, preserve this context after compaction. This is pushed into `output.context` (not `output.system`) so it's part of the compaction output, not the permanent system prompt.
31
+
32
+ ## Interactions with other features
33
+
34
+ - Nudge pipeline ([nudge-pipeline.md](nudge-pipeline.md)): compaction guard is the first check in the chat.message hook; suppresses all 4 tiers
35
+ - Session lifecycle ([session-lifecycle.md](session-lifecycle.md)): `session.compacted` event is handled in the event handler
36
+ - Multi-host ([multi-host.md](multi-host.md)): compaction recovery is opencode-only. Claude Code and Cursor do not have this feature. Mitigation: `CLAUDE.md`/`AGENTS.md` persists through compaction, and SessionStart hooks fire after compaction.
37
+
38
+ ## Source files
39
+
40
+ - `src/index.ts` -- all four hooks/events (experimental.session.compacting, experimental.compaction.autocontinue, session.compacted event, chat.message compaction guard)
41
+ - `src/prompts.ts` -- compactionContext function
42
+
43
+ ## Key invariants
44
+
45
+ - The `compacting` set is checked at the top of chat.message. If set, all nudges are skipped.
46
+ - Three mechanisms clear the flag (autocontinue, session.compacted event, chat.message fallback). Belt-and-suspenders.
47
+ - Compaction failure fallback: a non-compaction message during compacting = compaction failed. Clear and proceed.
48
+ - opencode-only feature. MCP hosts rely on static instructions persisting through compaction.
@@ -0,0 +1,244 @@
1
+ # Database
2
+
3
+ Thatch stores all data in a single SQLite file. The schema has 11 tables organized around three concerns: memory entries, the prediction engine, and the behavior engine.
4
+
5
+ ## Configuration
6
+
7
+ - Default path: `$XDG_CONFIG_HOME/thatch/thatch.db` (override via `THATCH_DB_PATH`)
8
+ - PRAGMAs: `journal_mode=WAL`, `busy_timeout=5000`, `foreign_keys=ON`
9
+ - Single file, no external services
10
+
11
+ ## Schema overview
12
+
13
+ 11 tables in three groups:
14
+
15
+ | Group | Tables | Purpose |
16
+ |-------|--------|---------|
17
+ | Memory | `stores`, `entries`, `dedup_pairs` | Core memory CRUD + dedup verdicts |
18
+ | Prediction engine | `prediction_matchers`, `predictions`, `prediction_edges`, `prediction_provenance` | User decision model |
19
+ | Behavior engine | `behavior_matchers`, `behaviors`, `behavior_edges`, `behavior_provenance` | LLM self-discipline rules |
20
+
21
+ The prediction and behavior engines share the same four-table shape (matchers, items, edges, provenance) with different table names. They are separate because the semantics differ: predictions model what the user wants; behaviors model what the LLM should do.
22
+
23
+ ## Memory tables
24
+
25
+ ### stores
26
+
27
+ Registry of all store names. The `global` store is inserted automatically at schema init.
28
+
29
+ ```sql
30
+ stores(name TEXT PRIMARY KEY)
31
+ ```
32
+
33
+ ### entries
34
+
35
+ The core table. Memory entries with embeddings, metadata, and usage telemetry.
36
+
37
+ ```sql
38
+ entries(
39
+ slug TEXT NOT NULL,
40
+ store TEXT NOT NULL REFERENCES stores(name),
41
+ label TEXT NOT NULL,
42
+ content TEXT NOT NULL,
43
+ embedding BLOB,
44
+ model TEXT,
45
+ branch TEXT,
46
+ confidence INTEGER,
47
+ archived INTEGER NOT NULL DEFAULT 0,
48
+ created_at TEXT NOT NULL,
49
+ updated_at TEXT NOT NULL,
50
+ recall_count INTEGER NOT NULL DEFAULT 0,
51
+ last_recalled_at TEXT,
52
+ PRIMARY KEY (slug, store)
53
+ )
54
+ ```
55
+
56
+ - `slug`: derived from label via `slugify()`. Unicode letters/digits preserved. All-symbol labels fall back to hash.
57
+ - `embedding`: raw Float32Array bytes. Null if not yet embedded.
58
+ - `model`: embedding model tag (informational; dimension is the real discriminator).
59
+ - `branch`: git branch scope. Null = project-wide.
60
+ - `confidence`: 1-10 scale. Null = unset.
61
+ - `archived`: 0 = live, 1 = archived. Search, dedup, and staleness queries all exclude archived by default.
62
+ - `recall_count` / `last_recalled_at`: usage telemetry, stamped by `db.recall()` (not `db.search()`).
63
+
64
+ ### dedup_pairs
65
+
66
+ Records which duplicate-candidate pairs have been reviewed.
67
+
68
+ ```sql
69
+ dedup_pairs(
70
+ store TEXT NOT NULL,
71
+ slug_a TEXT NOT NULL,
72
+ slug_b TEXT NOT NULL,
73
+ status TEXT NOT NULL,
74
+ checked_at TEXT NOT NULL,
75
+ PRIMARY KEY (store, slug_a, slug_b)
76
+ )
77
+ ```
78
+
79
+ - Slugs stored sorted (a < b) for canonical matching.
80
+ - `status`: "duplicate", "supplement", "contradiction", or "unrelated".
81
+ - Overwriting or forgetting either memory clears the verdict (DELETE in `remember()` and `forgetEntry()`).
82
+
83
+ ## Prediction engine tables
84
+
85
+ ### prediction_matchers
86
+
87
+ Situation descriptions that trigger predictions. Embedded, cosine-matched at `chat.message`.
88
+
89
+ ```sql
90
+ prediction_matchers(
91
+ id TEXT PRIMARY KEY,
92
+ store TEXT NOT NULL REFERENCES stores(name),
93
+ description TEXT NOT NULL,
94
+ embedding BLOB,
95
+ model TEXT,
96
+ created_at TEXT NOT NULL,
97
+ updated_at TEXT NOT NULL
98
+ )
99
+ ```
100
+
101
+ ### predictions
102
+
103
+ User preference predictions with Bayesian confidence.
104
+
105
+ ```sql
106
+ predictions(
107
+ id TEXT PRIMARY KEY,
108
+ store TEXT NOT NULL REFERENCES stores(name),
109
+ statement TEXT NOT NULL,
110
+ rationale TEXT,
111
+ embedding BLOB,
112
+ model TEXT,
113
+ confidence REAL NOT NULL DEFAULT 0.5,
114
+ confirm_count REAL NOT NULL DEFAULT 0,
115
+ disconfirm_count REAL NOT NULL DEFAULT 0,
116
+ created_at TEXT NOT NULL,
117
+ updated_at TEXT NOT NULL
118
+ )
119
+ ```
120
+
121
+ - `confidence`: Bayesian posterior. `p = (confirm + K*P0) / (confirm + disconfirm + K)`, K=5, P0=0.5.
122
+ - `confirm_count` / `disconfirm_count`: REAL (fractional for soft signals, weight 0.25).
123
+
124
+ ### prediction_edges
125
+
126
+ Many-to-many links between matchers and predictions.
127
+
128
+ ```sql
129
+ prediction_edges(
130
+ matcher_id TEXT NOT NULL REFERENCES prediction_matchers(id) ON DELETE CASCADE,
131
+ prediction_id TEXT NOT NULL REFERENCES predictions(id) ON DELETE CASCADE,
132
+ weight REAL NOT NULL DEFAULT 1.0,
133
+ created_at TEXT NOT NULL,
134
+ PRIMARY KEY (matcher_id, prediction_id)
135
+ )
136
+ ```
137
+
138
+ ### prediction_provenance
139
+
140
+ Audit trail of every signal applied to a prediction.
141
+
142
+ ```sql
143
+ prediction_provenance(
144
+ id TEXT PRIMARY KEY,
145
+ prediction_id TEXT NOT NULL REFERENCES predictions(id) ON DELETE CASCADE,
146
+ signal TEXT NOT NULL,
147
+ detail TEXT,
148
+ created_at TEXT NOT NULL
149
+ )
150
+ ```
151
+
152
+ - `signal`: "confirm", "disconfirm", "soft", or "create".
153
+
154
+ ## Behavior engine tables
155
+
156
+ Same shape as prediction engine, with behavior-specific semantics.
157
+
158
+ ### behavior_matchers
159
+
160
+ ```sql
161
+ behavior_matchers(
162
+ id TEXT PRIMARY KEY,
163
+ store TEXT NOT NULL REFERENCES stores(name),
164
+ description TEXT NOT NULL,
165
+ embedding BLOB,
166
+ model TEXT,
167
+ created_at TEXT NOT NULL,
168
+ updated_at TEXT NOT NULL
169
+ )
170
+ ```
171
+
172
+ ### behaviors
173
+
174
+ ```sql
175
+ behaviors(
176
+ id TEXT PRIMARY KEY,
177
+ store TEXT NOT NULL REFERENCES stores(name),
178
+ statement TEXT NOT NULL,
179
+ rationale TEXT,
180
+ embedding BLOB,
181
+ model TEXT,
182
+ confidence REAL NOT NULL DEFAULT 0.5,
183
+ confirm_count REAL NOT NULL DEFAULT 0,
184
+ disconfirm_count REAL NOT NULL DEFAULT 0,
185
+ created_at TEXT NOT NULL,
186
+ updated_at TEXT NOT NULL
187
+ )
188
+ ```
189
+
190
+ ### behavior_edges
191
+
192
+ ```sql
193
+ behavior_edges(
194
+ matcher_id TEXT NOT NULL REFERENCES behavior_matchers(id) ON DELETE CASCADE,
195
+ behavior_id TEXT NOT NULL REFERENCES behaviors(id) ON DELETE CASCADE,
196
+ weight REAL NOT NULL DEFAULT 1.0,
197
+ created_at TEXT NOT NULL,
198
+ PRIMARY KEY (matcher_id, behavior_id)
199
+ )
200
+ ```
201
+
202
+ ### behavior_provenance
203
+
204
+ ```sql
205
+ behavior_provenance(
206
+ id TEXT PRIMARY KEY,
207
+ behavior_id TEXT NOT NULL REFERENCES behaviors(id) ON DELETE CASCADE,
208
+ signal TEXT NOT NULL,
209
+ detail TEXT,
210
+ created_at TEXT NOT NULL
211
+ )
212
+ ```
213
+
214
+ - `signal`: "confirm" (ham), "disconfirm" (spam), or "codify".
215
+
216
+ ## Schema migration
217
+
218
+ `recall_count`, `last_recalled_at`, and `archived` are added to pre-existing databases by an idempotent column migration at init. The migration uses `PRAGMA table_info` to check for the column's existence, then `ALTER TABLE ADD COLUMN` if missing. This handles databases created before these columns existed.
219
+
220
+ ## Embedding serialization
221
+
222
+ Embeddings are raw Float32Array bytes stored as BLOBs. Serialization honors `byteOffset`/`byteLength` — transformers.js can return views into larger tensor buffers, and serializing the whole backing buffer corrupts vectors. Always serialize the view's own bytes, not the underlying buffer.
223
+
224
+ ## Key invariants
225
+
226
+ - Single SQLite file, WAL mode, 5s busy timeout, foreign keys ON.
227
+ - Embedding spaces are discriminated by vector dimension, not model tag.
228
+ - Embedding serialization honors `byteOffset`/`byteLength` (views into larger buffers).
229
+ - Slugs are slugified labels; slug + store = composite PK.
230
+ - Dedup verdicts auto-clear when either memory is overwritten or forgotten.
231
+ - Archived memories excluded from search, dedup, and staleness by default.
232
+ - Column migration is idempotent (`PRAGMA table_info` + `ALTER TABLE`).
233
+
234
+ ## Interactions with other features
235
+
236
+ - Memory store ([memory-store.md](memory-store.md)): `entries` table is the core; `stores` table is the registry
237
+ - Deduplication ([deduplication.md](deduplication.md)): `dedup_pairs` table tracks reviewed pairs
238
+ - Prediction engine ([prediction-engine.md](prediction-engine.md)): 4 prediction tables
239
+ - Behavior engine ([behavior-engine.md](behavior-engine.md)): 4 behavior tables
240
+ - Hygiene ([hygiene.md](hygiene.md)): `staleEntryCount` uses `recall_count` and `last_recalled_at`; `branchesInStore` uses `branch` column
241
+
242
+ ## Source files
243
+
244
+ - `src/db.ts` — all schema, CRUD, search, scoring, migrations
@@ -0,0 +1,102 @@
1
+ # Deduplication System
2
+
3
+ Agent-driven detection and resolution of duplicate memories. The system
4
+ surfaces candidates; the agent decides what to do.
5
+
6
+ ## What it does
7
+
8
+ - `find_duplicates` tool: cosine similarity pairs, grouped into
9
+ connected-component clusters
10
+ - `dedup_mark_checked` tool: records a pairwise verdict so `find_duplicates`
11
+ stops re-reporting
12
+ - Automatic verdict clearing: overwriting either memory clears the verdict
13
+ - `thatch-dedup-classifier` skill: classifies the relationship and reconciles
14
+ pairs
15
+
16
+ ## How it works
17
+
18
+ ### find_duplicates (tool)
19
+
20
+ - Parameters: `store` (optional, default project store), `threshold`
21
+ (optional, default 0.85)
22
+ - Finds memory pairs whose embeddings exceed the cosine similarity threshold
23
+ - Groups related pairs into connected-component clusters: a cluster of 3+
24
+ labels usually means one topic fragmented across entries that should be
25
+ consolidated
26
+ - Pairs already resolved via `dedup_mark_checked` are skipped
27
+ - Output: clustered rendering — "Cluster of N:" then
28
+ `[score:N] labelA <-> labelB` lines
29
+ - Read-only
30
+
31
+ ### dedup_mark_checked (tool)
32
+
33
+ - Parameters: `label_a`, `label_b` (both required), `status` (one of
34
+ `duplicate`, `supplement`, `contradiction`, `unrelated`), `store` (optional)
35
+ - Records a pairwise verdict so `find_duplicates` stops re-reporting that pair
36
+ - Labels are slugified internally
37
+ - Overwriting either memory later clears the verdict automatically
38
+ (DB-level: the `remember()` and `forgetEntry()` functions `DELETE` from
39
+ `dedup_pairs` for the affected slug)
40
+ - Returns `[checked] store :: a <-> b -> status`
41
+
42
+ ### Verdict lifecycle
43
+
44
+ 1. `find_duplicates` surfaces a pair above threshold
45
+ 2. Agent loads the `thatch-dedup-classifier` skill
46
+ 3. Agent classifies the pair: `duplicate` (merge), `supplement` (keep both),
47
+ `contradiction` (resolve), `unrelated` (mark and ignore)
48
+ 4. For duplicates: agent merges via `memory_remember(overwrite: true)` +
49
+ `memory_forget` the other entry
50
+ 5. Agent calls `dedup_mark_checked` for the surviving pair
51
+ 6. Overwriting either memory clears the verdict → the pair can re-flag if the
52
+ merge created a new similarity
53
+
54
+ ### The dedup_pairs table
55
+
56
+ - PK: `(store, slug_a, slug_b)` — slugs stored sorted (`a < b`) for canonical
57
+ matching
58
+ - No FK to entries (slugs are compared by string, not by FK constraint)
59
+ - Overwriting or forgetting an entry clears all verdicts involving that slug
60
+ (`DELETE` in `remember()` and `forgetEntry()`)
61
+
62
+ ### thatch-dedup-classifier skill
63
+
64
+ - Shared skill (`artifacts/skills/thatch-dedup-classifier.md`)
65
+ - Classifies the relationship between two similar memory entries
66
+ - Provides a structured process: read both entries, compare content, decide
67
+ verdict, reconcile
68
+ - Can be dispatched as a sub-agent by the extraction pipeline or loaded
69
+ directly by the agent
70
+
71
+ ## Interactions with other features
72
+
73
+ - [Memory store](memory-store.md): `find_duplicates` searches stored memories;
74
+ merging uses `memory_remember(overwrite)` + `memory_forget`.
75
+ - [Hygiene](hygiene.md): hygiene report counts pending dedup pairs (same
76
+ underlying `db.findDuplicates`).
77
+ - Skills: `thatch-dedup-classifier` is the skill that guides the
78
+ reconciliation process.
79
+ - [Extraction](extraction.md): extraction can trigger the dedup cycle when
80
+ write-time duplicate detection fires.
81
+
82
+ ## Source files
83
+
84
+ - `src/db.ts` — `findDuplicates`, `markChecked`, `dedup_pairs` table
85
+ - `src/tool-defs.ts` — 2 dedup tools (`find_duplicates`, `dedup_mark_checked`)
86
+ - `artifacts/skills/thatch-dedup-classifier.md` — classifier skill
87
+
88
+ ## Database tables
89
+
90
+ - `dedup_pairs(store, slug_a, slug_b, status, checked_at, PK(store, slug_a, slug_b))`
91
+
92
+ ## Key invariants
93
+
94
+ - `find_duplicates` groups pairs into clusters (connected components). A
95
+ cluster of 3+ should be consolidated into one memory.
96
+ - Verdicts are automatically cleared when either memory is overwritten or
97
+ forgotten. This is intended — a merge can create a new similarity that
98
+ should re-flag.
99
+ - The threshold (0.85) is higher than the recall nudge threshold (0.55) because
100
+ "duplicate" is a stronger claim than "relates to".
101
+ - Slugs in `dedup_pairs` are stored sorted (`a < b`) for canonical matching
102
+ regardless of argument order.