@jeffober/thatch 0.1.10 → 0.1.12

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.
@@ -21,9 +21,9 @@ description: Extract durable project facts ... Use when ...
21
21
  - `description` drives when the agent loads the skill (both opencode and
22
22
  Cursor auto-discover skills and use the description for relevance).
23
23
 
24
- ## The 13 skills
24
+ ## The 14 skills
25
25
 
26
- **Shared (12)** — installed everywhere; no sub-agents required:
26
+ **Shared (13)** — installed everywhere; no sub-agents required:
27
27
 
28
28
  | Skill | Role |
29
29
  |-------|------|
@@ -35,6 +35,7 @@ description: Extract durable project facts ... Use when ...
35
35
  | `thatch-review-state-flow` | Data flow and contracts: module boundaries, implicit FSMs, error propagation. |
36
36
  | `thatch-review-no-slop` | AI writing anti-patterns: change narration, fourth-wall breaks, em dashes, filler. |
37
37
  | `thatch-review-breadcrumbs` | Comment narrative: do comments form a coherent outline of behavior? |
38
+ | `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
38
39
  | `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. |
39
40
  | `thatch-review-context` | Gather project/feature context (PR descriptions, git archaeology, ticket references, memory) before fan-out. Prevents false positives about intentionally deferred work. |
40
41
  | `thatch-workflow-research` | Research code workflows/features affected by a change or planned change. Reads code flows, comments, git history, memories, docs. Produces a guide to the code for reviewers or planners. |
@@ -44,11 +45,11 @@ description: Extract durable project facts ... Use when ...
44
45
 
45
46
  | Skill | Role |
46
47
  |-------|------|
47
- | `thatch-code-review` | Resolve review target, gather project context, research affected workflows, estimate complexity, partition, dispatch the 5 specialists in parallel, synthesize. |
48
+ | `thatch-code-review` | Resolve review target, gather project context, research affected workflows, estimate complexity, partition, dispatch the 6 specialists in parallel, synthesize. |
48
49
 
49
50
  ## REVIEW_COMMON
50
51
 
51
- The five review specialists share a framework interpolated via `${REVIEW_COMMON}`:
52
+ The six review specialists share a framework interpolated via `${REVIEW_COMMON}`:
52
53
 
53
54
  - **Static analysis only** — no running tests/linters/compilers.
54
55
  - **Scope gathering** — resolve the git range, `git diff --stat`, read changed
@@ -78,7 +79,7 @@ The synthesizer reuses the same verification rigor but has its own structure
78
79
  ## The two arrays
79
80
 
80
81
  ```ts
81
- const SHARED_SKILLS: SkillDef[] = [ /* 12 skills above */ ];
82
+ const SHARED_SKILLS: SkillDef[] = [ /* 13 skills above */ ];
82
83
  const OPENCODE_ONLY_SKILLS: SkillDef[] = [ /* code-review coordinator */ ];
83
84
  ```
84
85
 
@@ -112,7 +113,7 @@ and `--cursor` pass only the shared set.
112
113
  ## Memory review skills in practice
113
114
 
114
115
  - **Quick single lens**: load any specialist directly and point it at a branch.
115
- - **Full review on opencode**: load `thatch-code-review` — it gathers project context, researches affected workflows, dispatches all 5 specialists in parallel (with both the context brief and workflow guide injected into each briefing), then synthesizes.
116
+ - **Full review on opencode**: load `thatch-code-review` — it gathers project context, researches affected workflows, dispatches all 6 specialists in parallel (with both the context brief and workflow guide injected into each briefing), then synthesizes.
116
117
  - **Full review on Claude Code/Cursor**: run each specialist in sequence, then
117
118
  run `thatch-review-synthesizer` to verify and aggregate.
118
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
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",
package/src/db.ts CHANGED
@@ -9,6 +9,7 @@ export interface MemoryRow {
9
9
  model: string | null;
10
10
  branch: string | null;
11
11
  confidence: number | null;
12
+ archived: boolean;
12
13
  created_at: string;
13
14
  updated_at: string;
14
15
  recall_count: number;
@@ -69,6 +70,7 @@ export class ThatchDB {
69
70
  model TEXT,
70
71
  branch TEXT,
71
72
  confidence INTEGER,
73
+ archived INTEGER NOT NULL DEFAULT 0,
72
74
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
73
75
  updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
74
76
  recall_count INTEGER NOT NULL DEFAULT 0,
@@ -102,6 +104,7 @@ export class ThatchDB {
102
104
  const wanted: [string, string][] = [
103
105
  ["recall_count", "INTEGER NOT NULL DEFAULT 0"],
104
106
  ["last_recalled_at", "TEXT"],
107
+ ["archived", "INTEGER NOT NULL DEFAULT 0"],
105
108
  ];
106
109
  for (const [col, decl] of wanted) {
107
110
  if (!existing.has(col)) {
@@ -170,27 +173,23 @@ export class ThatchDB {
170
173
  content: string,
171
174
  embedding: Float32Array,
172
175
  model: string,
173
- opts?: { branch?: string; confidence?: number; overwrite?: boolean },
176
+ opts?: { branch?: string; confidence?: number; overwrite?: boolean; archived?: boolean },
174
177
  ): { ok: true } | { ok: false; error: string } {
175
178
  const slug = this.slugify(label);
176
179
  this.ensureStore(store);
177
180
 
178
- // The embedding may be a view into a larger buffer (transformers.js tensor
179
- // output); serialize exactly the view's bytes, not the whole backing buffer.
180
181
  const blob = new Uint8Array(embedding.buffer, embedding.byteOffset, embedding.byteLength);
181
182
  const branch = opts?.branch ?? null;
182
183
  const confidence = opts?.confidence ?? null;
184
+ const archived = opts?.archived === true ? 1 : (opts?.archived === false ? 0 : null);
183
185
  const now = new Date().toISOString();
184
- const params = [slug, store, label, content, blob, model, branch, confidence, now, now];
185
186
 
186
187
  if (!opts?.overwrite) {
187
- // Plain INSERT so the PK constraint enforces no-clobber atomically —
188
- // a check-then-upsert would race against concurrent sessions.
189
188
  try {
190
189
  this.#db.run(
191
- `INSERT INTO entries (slug, store, label, content, embedding, model, branch, confidence, created_at, updated_at)
192
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
193
- params as any,
190
+ `INSERT INTO entries (slug, store, label, content, embedding, model, branch, confidence, archived, created_at, updated_at)
191
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
192
+ [slug, store, label, content, blob, model, branch, confidence, archived ?? 0, now, now] as any,
194
193
  );
195
194
  return { ok: true };
196
195
  } catch (err: any) {
@@ -205,10 +204,25 @@ export class ThatchDB {
205
204
  }
206
205
  }
207
206
 
207
+ const existing = this.#db
208
+ .query("SELECT archived FROM entries WHERE slug = ? AND store = ?")
209
+ .get(slug, store) as { archived: number } | null;
210
+
211
+ if (existing && existing.archived && opts?.archived === undefined) {
212
+ return {
213
+ ok: false,
214
+ error: `"${label}" is archived. Pass archived: true to keep it archived, or archived: false to unarchive it.`,
215
+ };
216
+ }
217
+
218
+ // archived column for upsert: the INSERT VALUES always needs a non-null
219
+ // value (NOT NULL constraint), so use archived ?? 0. To preserve the
220
+ // existing value when the caller doesn't specify archived, the UPDATE
221
+ // clause accepts a nullable extra param: NULL means "keep existing."
208
222
  this.#db.run(
209
223
  `
210
- INSERT INTO entries (slug, store, label, content, embedding, model, branch, confidence, created_at, updated_at)
211
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
224
+ INSERT INTO entries (slug, store, label, content, embedding, model, branch, confidence, archived, created_at, updated_at)
225
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
212
226
  ON CONFLICT(slug, store) DO UPDATE SET
213
227
  label = excluded.label,
214
228
  content = excluded.content,
@@ -216,13 +230,12 @@ export class ThatchDB {
216
230
  model = excluded.model,
217
231
  branch = COALESCE(excluded.branch, entries.branch),
218
232
  confidence = COALESCE(excluded.confidence, entries.confidence),
233
+ archived = COALESCE(?, entries.archived),
219
234
  updated_at = excluded.updated_at
220
235
  `,
221
- params as any,
236
+ [slug, store, label, content, blob, model, branch, confidence, archived ?? 0, now, now, archived] as any,
222
237
  );
223
238
 
224
- // Content changed, so prior dedup verdicts involving this entry are stale;
225
- // clear them so findDuplicates can re-flag the pair.
226
239
  this.#db.run(
227
240
  "DELETE FROM dedup_pairs WHERE store = ? AND (slug_a = ? OR slug_b = ?)",
228
241
  [store, slug, slug],
@@ -242,22 +255,25 @@ export class ThatchDB {
242
255
  search(
243
256
  stores: string[],
244
257
  queryEmbedding: Float32Array,
245
- opts?: { branch?: string; limit?: number },
258
+ opts?: { branch?: string; limit?: number; includeArchived?: boolean },
246
259
  ): (MemoryRow & { _score: number })[] {
247
260
  if (stores.length === 0) return [];
248
261
 
249
262
  const limit = opts?.limit ?? 10;
250
263
  const branch = opts?.branch;
264
+ const includeArchived = opts?.includeArchived ?? false;
251
265
 
252
266
  const placeholders = stores.map(() => "?").join(", ");
253
267
 
254
268
  interface SqlParams { sql: string; params: any[] }
255
269
 
256
270
  const { sql, params }: SqlParams = (() => {
271
+ const clauses = ["store IN (" + placeholders + ")", "embedding IS NOT NULL"];
272
+ if (!includeArchived) clauses.push("archived = 0");
257
273
  const base = `
258
- SELECT slug, store, label, content, embedding, model, branch, confidence, created_at, updated_at, recall_count, last_recalled_at
274
+ SELECT slug, store, label, content, embedding, model, branch, confidence, archived, created_at, updated_at, recall_count, last_recalled_at
259
275
  FROM entries
260
- WHERE store IN (${placeholders}) AND embedding IS NOT NULL
276
+ WHERE ${clauses.join(" AND ")}
261
277
  `;
262
278
 
263
279
  if (branch) {
@@ -278,6 +294,7 @@ export class ThatchDB {
278
294
  // rather than ranked. Dimension is the discriminator — model tags are
279
295
  // informational only.
280
296
  const scored = rows.flatMap((row) => {
297
+ row.archived = !!row.archived;
281
298
  const emb = blobToVector(row.embedding!);
282
299
  if (emb.length !== queryEmbedding.length) return [];
283
300
  return [{ ...row, embedding: row.embedding, _score: cosineSimilarity(queryEmbedding, emb) }];
@@ -295,7 +312,7 @@ export class ThatchDB {
295
312
  recall(
296
313
  stores: string[],
297
314
  queryEmbedding: Float32Array,
298
- opts?: { branch?: string; limit?: number },
315
+ opts?: { branch?: string; limit?: number; includeArchived?: boolean },
299
316
  ): (MemoryRow & { _score: number })[] {
300
317
  const top = this.search(stores, queryEmbedding, opts);
301
318
 
@@ -326,7 +343,7 @@ export class ThatchDB {
326
343
  const limit = opts?.limit ?? 3;
327
344
 
328
345
  const rows = this.#db
329
- .query("SELECT slug, label, embedding FROM entries WHERE store = ? AND embedding IS NOT NULL")
346
+ .query("SELECT slug, label, embedding FROM entries WHERE store = ? AND archived = 0 AND embedding IS NOT NULL")
330
347
  .all(store) as any[];
331
348
 
332
349
  const hits: SimilarEntry[] = [];
@@ -345,10 +362,10 @@ export class ThatchDB {
345
362
  }
346
363
 
347
364
  /** Lists all entries in a store, returning metadata without content. */
348
- listEntries(store: string): { slug: string; label: string; branch: string | null; confidence: number | null; updated_at: string }[] {
365
+ listEntries(store: string): { slug: string; label: string; branch: string | null; confidence: number | null; archived: boolean; updated_at: string }[] {
349
366
  return this.#db
350
367
  .query(
351
- "SELECT slug, label, branch, confidence, updated_at FROM entries WHERE store = ? ORDER BY label",
368
+ "SELECT slug, label, branch, confidence, archived, updated_at FROM entries WHERE store = ? ORDER BY label",
352
369
  )
353
370
  .all(store)
354
371
  .map((r: any) => ({
@@ -356,6 +373,7 @@ export class ThatchDB {
356
373
  label: r.label,
357
374
  branch: r.branch,
358
375
  confidence: r.confidence,
376
+ archived: !!r.archived,
359
377
  updated_at: r.updated_at,
360
378
  }));
361
379
  }
@@ -363,11 +381,14 @@ export class ThatchDB {
363
381
  /** Full content of a single entry by label. */
364
382
  showEntry(store: string, label: string): MemoryRow | null {
365
383
  const slug = this.slugify(label);
366
- return this.#db
384
+ const row = this.#db
367
385
  .query(
368
- "SELECT slug, store, label, content, embedding, model, branch, confidence, created_at, updated_at, recall_count, last_recalled_at FROM entries WHERE slug = ? AND store = ?",
386
+ "SELECT slug, store, label, content, embedding, model, branch, confidence, archived, created_at, updated_at, recall_count, last_recalled_at FROM entries WHERE slug = ? AND store = ?",
369
387
  )
370
- .get(slug, store) as MemoryRow | null;
388
+ .get(slug, store) as Record<string, unknown> | null;
389
+ if (!row) return null;
390
+ row.archived = !!row.archived;
391
+ return row as unknown as MemoryRow;
371
392
  }
372
393
 
373
394
  forgetEntry(store: string, label: string): boolean {
@@ -386,7 +407,7 @@ export class ThatchDB {
386
407
  findDuplicates(store: string, threshold = 0.85): DedupCandidate[] {
387
408
  const rows = this.#db
388
409
  .query(
389
- "SELECT slug, label, content, embedding FROM entries WHERE store = ? AND embedding IS NOT NULL ORDER BY slug",
410
+ "SELECT slug, label, content, embedding FROM entries WHERE store = ? AND archived = 0 AND embedding IS NOT NULL ORDER BY slug",
390
411
  )
391
412
  .all(store) as any[];
392
413
 
@@ -446,7 +467,7 @@ export class ThatchDB {
446
467
  const row = this.#db
447
468
  .query(
448
469
  `SELECT COUNT(*) AS n FROM entries
449
- WHERE store = ? AND max(updated_at, COALESCE(last_recalled_at, updated_at)) < ?`,
470
+ WHERE store = ? AND archived = 0 AND max(updated_at, COALESCE(last_recalled_at, updated_at)) < ?`,
450
471
  )
451
472
  .get(store, cutoffIso) as any;
452
473
  return row?.n ?? 0;
@@ -465,7 +486,7 @@ export class ThatchDB {
465
486
  if (branches.length === 0) return 0;
466
487
  const placeholders = branches.map(() => "?").join(", ");
467
488
  const row = this.#db
468
- .query(`SELECT COUNT(*) AS n FROM entries WHERE store = ? AND branch IN (${placeholders})`)
489
+ .query(`SELECT COUNT(*) AS n FROM entries WHERE store = ? AND archived = 0 AND branch IN (${placeholders})`)
469
490
  .get(store, ...branches) as any;
470
491
  return row?.n ?? 0;
471
492
  }
package/src/prompts.ts CHANGED
@@ -32,7 +32,7 @@ you're working in. Prior sessions may have already investigated it.
32
32
  Thatch ships skills for code review, project investigation, and memory
33
33
  workflows. The host auto-discovers them, but reach for them proactively:
34
34
 
35
- - \`thatch-code-review\` — full multi-agent code review (dispatches 5 specialists + synthesizer). Requires sub-agent support.
35
+ - \`thatch-code-review\` — full multi-agent code review (dispatches 6 specialists + synthesizer). Requires sub-agent support.
36
36
  - \`thatch-workflow-research\` — research code workflows affected by a change or planned change, before reviewing or planning.
37
37
  - \`thatch-review-context\` — gather project context (PRs, tickets, TODOs, deferred work) before a review.
38
38
  - \`thatch-project-primer\` — investigate a new project and write foundational memories.
@@ -69,6 +69,25 @@ new project knowledge, conventions, architectural decisions, investigation resul
69
69
  Session-specific context, incomplete/unverified info, anything already in
70
70
  CLAUDE.md or OPENCODE.md, speculative conclusions.
71
71
 
72
+ ## Archived Memories
73
+
74
+ Memories can be marked \`archived\` — a flag for stable, long-term historical
75
+ records that should not trigger hygiene nudges (stale, orphaned, duplicate).
76
+ Archived memories are excluded from search/recall results by default; pass
77
+ \`includeArchived: true\` to surface them for archaeological dives.
78
+
79
+ When a branch is merged or about to be deleted, consolidate its branch-scoped
80
+ memories into a single archived memory scoped to that same branch (preserving
81
+ provenance). Capture intent, design decisions, review back-and-forth, PR
82
+ number, unexpected pivots — the kind of git-archaeology context that explains
83
+ _why_ the code looks the way it does a year from now. Then memory_forget the
84
+ originals. The archived record outlives the branch. Future sessions searching
85
+ with \`includeArchived: true\` can pull it up when investigating ambiguous code.
86
+
87
+ Updating an archived memory requires passing \`archived\` explicitly — omit it
88
+ and the tool returns an error. Pass \`archived: true\` to keep it archived,
89
+ \`archived: false\` to unarchive.
90
+
72
91
  ## Explicit Requests
73
92
 
74
93
  "Remember X" — save immediately.
@@ -94,7 +113,7 @@ Use thatch_store_list then thatch_memory_list to see what's available, then that
94
113
  return `${base}
95
114
 
96
115
  [thatch hygiene] ${hygiene}
97
- When convenient this session (not before addressing the user's request), tend the store: run thatch_find_duplicates and consolidate clusters, review stale memories with thatch_memory_show and update or forget them, and forget memories scoped to deleted branches.`;
116
+ When convenient this session (not before addressing the user's request), tend the store: run thatch_find_duplicates and consolidate clusters, review stale memories with thatch_memory_show and update or forget them, and for memories scoped to deleted branches, consolidate them into an archived historical record before forgetting the originals.`;
98
117
  }
99
118
 
100
119
  /**
@@ -146,7 +165,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
146
165
 
147
166
  - \`thatch-workflow-research\` — research code workflows affected by a change or planned change, before reviewing or planning.
148
167
  - \`thatch-review-context\` — gather project context (PRs, tickets, TODOs, deferred work) before a review.
149
- - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` — five specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
168
+ - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` / \`-mark-and-sweep\` — six specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
150
169
  - \`thatch-project-primer\` — investigate a new project and write foundational memories.
151
170
  - \`thatch-session-reflection\` — record what you learned at end of session.
152
171
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
@@ -181,6 +200,25 @@ new project knowledge, conventions, architectural decisions, investigation resul
181
200
  Session-specific context, incomplete/unverified info, anything already in
182
201
  CLAUDE.md, speculative conclusions.
183
202
 
203
+ ## Archived Memories
204
+
205
+ Memories can be marked \`archived\` — a flag for stable, long-term historical
206
+ records that should not trigger hygiene nudges (stale, orphaned, duplicate).
207
+ Archived memories are excluded from search/recall results by default; pass
208
+ \`includeArchived: true\` to surface them for archaeological dives.
209
+
210
+ When a branch is merged or about to be deleted, consolidate its branch-scoped
211
+ memories into a single archived memory scoped to that same branch (preserving
212
+ provenance). Capture intent, design decisions, review back-and-forth, PR
213
+ number, unexpected pivots — the kind of git-archaeology context that explains
214
+ _why_ the code looks the way it does a year from now. Then memory_forget the
215
+ originals. The archived record outlives the branch. Future sessions searching
216
+ with \`includeArchived: true\` can pull it up when investigating ambiguous code.
217
+
218
+ Updating an archived memory requires passing \`archived\` explicitly — omit it
219
+ and the tool returns an error. Pass \`archived: true\` to keep it archived,
220
+ \`archived: false\` to unarchive.
221
+
184
222
  ## Explicit Requests
185
223
 
186
224
  "Remember X" — save immediately.
@@ -205,7 +243,7 @@ Use store_list then memory_list to see what's available, then memory_recall as n
205
243
  return `${base}
206
244
 
207
245
  [thatch hygiene] ${hygiene}
208
- When convenient this session (not before addressing the user's request), tend the store: run find_duplicates and consolidate clusters, review stale memories with memory_show and update or forget them, and forget memories scoped to deleted branches.`;
246
+ When convenient this session (not before addressing the user's request), tend the store: run find_duplicates and consolidate clusters, review stale memories with memory_show and update or forget them, and for memories scoped to deleted branches, consolidate them into an archived historical record before forgetting the originals.`;
209
247
  }
210
248
 
211
249
  /**
@@ -274,7 +312,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
274
312
 
275
313
  - \`thatch-workflow-research\` — research code workflows affected by a change or planned change, before reviewing or planning.
276
314
  - \`thatch-review-context\` — gather project context (PRs, tickets, TODOs, deferred work) before a review.
277
- - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` — five specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
315
+ - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` / \`-mark-and-sweep\` — six specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
278
316
  - \`thatch-project-primer\` — investigate a new project and write foundational memories.
279
317
  - \`thatch-session-reflection\` — record what you learned at end of session.
280
318
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
@@ -309,6 +347,25 @@ new project knowledge, conventions, architectural decisions, investigation resul
309
347
  Session-specific context, incomplete/unverified info, anything already in
310
348
  AGENTS.md, speculative conclusions.
311
349
 
350
+ ## Archived Memories
351
+
352
+ Memories can be marked \`archived\` — a flag for stable, long-term historical
353
+ records that should not trigger hygiene nudges (stale, orphaned, duplicate).
354
+ Archived memories are excluded from search/recall results by default; pass
355
+ \`includeArchived: true\` to surface them for archaeological dives.
356
+
357
+ When a branch is merged or about to be deleted, consolidate its branch-scoped
358
+ memories into a single archived memory scoped to that same branch (preserving
359
+ provenance). Capture intent, design decisions, review back-and-forth, PR
360
+ number, unexpected pivots — the kind of git-archaeology context that explains
361
+ _why_ the code looks the way it does a year from now. Then memory_forget the
362
+ originals. The archived record outlives the branch. Future sessions searching
363
+ with \`includeArchived: true\` can pull it up when investigating ambiguous code.
364
+
365
+ Updating an archived memory requires passing \`archived\` explicitly — omit it
366
+ and the tool returns an error. Pass \`archived: true\` to keep it archived,
367
+ \`archived: false\` to unarchive.
368
+
312
369
  ## Explicit Requests
313
370
 
314
371
  "Remember X" — save immediately.
package/src/skills.ts CHANGED
@@ -652,6 +652,133 @@ Good section headers create a table of contents:
652
652
  Do NOT report on files you did not actually read.
653
653
  `;
654
654
 
655
+ const REVIEW_MARK_AND_SWEEP = `---
656
+ name: thatch-review-mark-and-sweep
657
+ description: Mechanical change completeness audit — verifies that renames, flag removals, API substitutions, and mass replacements left no stragglers. Sweeps the whole repo, not just changed files. Use for post-implementation review of a branch, PR, or commit range.
658
+ ---
659
+
660
+ You are a mark-and-sweep review agent. You verify that mechanical changes — renames, feature flag removals, API/module substitutions, mass replacements — achieved closure: every occurrence of the old identifier was updated, no dead branches or orphaned references remain, and touch points were neatened with appropriate comments.
661
+ ${REVIEW_COMMON}
662
+ ## When to sweep
663
+
664
+ Not every change is a mechanical change. Before sweeping, determine whether the diff shows patterns of mechanical change:
665
+
666
+ **Sweep triggers:**
667
+ - Same identifier removed or replaced across multiple files
668
+ - Import substitution (old module replaced with new module)
669
+ - Renamed function, module, class, or variable (old name to new name)
670
+ - Removed config key, feature flag, or environment variable
671
+ - Deleted function/module/class that may have callers elsewhere
672
+
673
+ **No sweep needed:**
674
+ - Bug fixes (changing logic, not removing identifiers)
675
+ - New features (adding code, not removing identifiers)
676
+ - Behavioral refactors (changing what code does, not what it's called)
677
+
678
+ If no mechanical change is detected, report: "No mechanical change detected — nothing to sweep." Do not force a sweep on a diff that has nothing to mark.
679
+
680
+ ## Your focus
681
+
682
+ You care about:
683
+ - **Stragglers**: Old identifiers still referenced in files the diff did not touch
684
+ - **Dead branches**: Conditional branches now unreachable because their guard or flag was removed but the branch was not collapsed
685
+ - **Stale comments**: Comments that reference the old identifier, old behavior, or old config that no longer exists
686
+ - **Orphaned imports**: Imports of the old module or function that are no longer used after the change
687
+ - **Config residue**: Default values, config schemas, or env var docs for removed identifiers
688
+ - **Test residue**: Test fixtures or test cases that still set removed flags, call removed functions, or import removed modules
689
+
690
+ You do NOT care about:
691
+ - Whether the new code is correct (other reviewers handle logic)
692
+ - UX or behavioral concerns
693
+ - Code style or formatting
694
+ - Comment narrative quality
695
+
696
+ ## The mark-and-sweep method
697
+
698
+ Your scope is NOT limited to changed files. The scope gathering steps above identify what changed and give you the diffs you need to extract old identifiers. The sweep below extends across the entire repository.
699
+
700
+ ### 1. Mark — extract old identifiers from the diff
701
+
702
+ For each changed file, read the diff (\`git diff <range> -- <file>\`). From the removed lines (starting with \`-\`), extract identifiers that were removed, renamed, or replaced:
703
+
704
+ - **Function and method definitions**: \`def old_name\`, \`function old_name\`, \`fn old_name\`, \`const old_name\`, etc.
705
+ - **Imports**: \`import old_module\`, \`from old_module import ...\`, \`require("old_module")\`, \`use old_module\`, etc.
706
+ - **Variables and constants**: \`old_flag = ...\`, \`OLD_CONFIG_KEY = ...\`, etc.
707
+ - **Renames**: If an old name appears on a \`-\` line and a new name on the corresponding \`+\` line, mark the old name. The new name tells you what to expect in updated files.
708
+ - **Config keys and feature flags**: Names of removed flags, config keys, or env vars from deleted lines.
709
+
710
+ Also read commit messages (\`git log --oneline <range>\`) for intent: "remove X", "rename A to B", "replace Y with Z", "delete deprecated Z".
711
+
712
+ Build a mark list: each entry is an old identifier and what replaced it (if anything).
713
+
714
+ ### 2. Sweep — search the entire repository for each marked identifier
715
+
716
+ For each identifier in the mark list, run a whole-repo search:
717
+ - \`git grep -n "<identifier>"\` or \`rg -n "<identifier>"\` across the entire repository
718
+ - Exclude: vendored dependencies, generated files, lockfiles, compiled assets, node_modules, .git
719
+
720
+ For each hit, classify it:
721
+ - **In a changed file, still present**: The old identifier appears in a changed file but was not updated. This is a finding — the change was incomplete even within the files it touched.
722
+ - **In an unchanged file**: This is the primary finding type. The sweep missed this occurrence entirely.
723
+ - **False positive**: Substring match, different namespace, homonym, or unrelated use. Filter these out.
724
+
725
+ For common-word identifiers, use word-boundary patterns or filter by context. A reference to a removed \`process\` function is a straggler; the word "process" in a comment about CPU scheduling is not.
726
+
727
+ ### 3. Touchpoint neatness — verify change sites were cleaned up
728
+
729
+ For each changed file where an old identifier was removed or replaced:
730
+ - **Dead branches**: If a conditional guard was removed (e.g., \`if feature_flag_enabled\`), was the entire conditional collapsed or just the guard line? Is the else branch now dead code that should be removed?
731
+ - **Orphaned imports**: Does the old import still appear even though the imported name is no longer used?
732
+ - **Stale comments**: Do comments near the change site still reference the old name, old behavior, or old config?
733
+ - **Config residue**: Are there default values, schema entries, or env var documentation for the removed identifier?
734
+ - **Test residue**: Do test fixtures still set removed flags, call removed functions, or import removed modules?
735
+
736
+ ### 4. Verify intent for each finding
737
+
738
+ A straggler may be intentional:
739
+ - **Backward compatibility shim**: The old name is kept for external callers. Check git history and comments for evidence.
740
+ - **Deprecated module**: Scheduled for removal in a future PR. Check for TODO ($ticket) markers.
741
+ - **Separate cleanup PR**: The removal was split across PRs. Check the project context brief for deferred work.
742
+
743
+ Use the intent verification steps (trace callers, check git history, check memories) before reporting. If the straggler is intentional or deferred, do not report it as a finding.
744
+
745
+ ## What is NOT a finding
746
+
747
+ - **Substring matches**: \`git grep "config"\` matching \`oldConfigValue\` when the marked identifier was \`Config\` (the class). Use word boundaries.
748
+ - **Homonyms**: The same word used in an unrelated context (e.g., \`process\` the function vs. "process" in documentation prose).
749
+ - **Different namespace**: An identifier with the same name in a different module or package that was not part of the change.
750
+ - **Intentional shims**: Backward compatibility wrappers with documented purpose or comments explaining why the old name is retained.
751
+ - **Deferred cleanup**: Marked with a TODO ($ticket) referencing an open ticket.
752
+ - **Vendored or generated code**: References in third-party or generated files that the project does not maintain.
753
+
754
+ ## Worked example
755
+
756
+ **Change**: PR removes the \`USE_NEW_VALIDATION\` feature flag. The diff shows \`config.ts\` (flag definition deleted) and \`validation.ts\` (conditional \`if USE_NEW_VALIDATION\` removed, new path is now unconditional).
757
+
758
+ **Mark**: \`USE_NEW_VALIDATION\`
759
+
760
+ **Sweep**: \`git grep -n "USE_NEW_VALIDATION"\` returns hits in:
761
+ - \`src/config.ts\` (changed — expected, the diff removed it here)
762
+ - \`tests/validation.test.ts\` (unchanged — STRAGGLER: test still sets the flag in a fixture)
763
+ - \`docs/config.md\` (unchanged — STRAGGLER: docs still describe the flag)
764
+ - \`.env.example\` (unchanged — CONFIG_RESIDUE: env var example still present)
765
+
766
+ **Touchpoint neatness**: In \`validation.ts\`, the diff removed the \`if USE_NEW_VALIDATION { ... } else { oldValidation() }\` conditional, but \`oldValidation()\` was left as a now-unreachable function (DEAD_BRANCH). The comment above it still says "fallback when USE_NEW_VALIDATION is disabled" (STALE_COMMENT).
767
+
768
+ **Findings**: 4 findings (2 STRAGGLER, 1 CONFIG_RESIDUE, 1 DEAD_BRANCH) plus 1 STALE_COMMENT at the touchpoint.
769
+
770
+ ## Category taxonomy
771
+
772
+ - **STRAGGLER**: Old identifier found in a file the diff did not touch — the sweep missed this occurrence
773
+ - **DEAD_BRANCH**: Code path now unreachable because its guard, flag, or condition was removed but the branch was not collapsed
774
+ - **STALE_COMMENT**: Comment references the old identifier, old behavior, or old config that no longer exists
775
+ - **ORPHANED_IMPORT**: Import of the old module or function remains but is no longer used after the change
776
+ - **CONFIG_RESIDUE**: Config keys, defaults, schema entries, or env var docs for a removed identifier
777
+ - **TEST_RESIDUE**: Test fixtures or test cases that reference removed identifiers
778
+
779
+ For straggler findings, the source of truth is the diff (the identifier was removed) and the grep hit (the identifier still exists elsewhere). Use "N/A — mechanical finding" for the producer chain.
780
+ `;
781
+
655
782
  // ---------------------------------------------------------------------------
656
783
  // Review synthesizer — verify, deduplicate, and aggregate specialist findings
657
784
  // ---------------------------------------------------------------------------
@@ -661,7 +788,7 @@ name: thatch-review-synthesizer
661
788
  description: Verify and synthesize findings from multiple review specialist skills into a single deduplicated, severity-grouped report. Use after running one or more thatch-review-* specialist skills.
662
789
  ---
663
790
 
664
- You are a review synthesizer. You have received findings from one or more review specialists (pedantic, acceptance, state-flow, no-slop, breadcrumbs). Your job is to verify their citations against the actual code, deduplicate across specialists, and produce a single, coherent final report.
791
+ You are a review synthesizer. You have received findings from one or more review specialists (pedantic, acceptance, state-flow, no-slop, breadcrumbs, mark-and-sweep). Your job is to verify their citations against the actual code, deduplicate across specialists, and produce a single, coherent final report.
665
792
 
666
793
  ## Static analysis only
667
794
  You review code by reading it. Do NOT run tests, linters, compilers, or any build commands.
@@ -1089,7 +1216,7 @@ Dispatch a sub-agent using the Task tool to research the code workflows affected
1089
1216
 
1090
1217
  The sub-agent produces a code guide with one section per affected workflow or feature. Wait for it to complete and collect the guide.
1091
1218
 
1092
- This guide gives specialists the code-level context (purpose, data flow, evolutions, constraints) they need to avoid false positives about intentional behavior and long-standing design decisions, and reduces duplicate code-tracing across the five specialist lenses.
1219
+ This guide gives specialists the code-level context (purpose, data flow, evolutions, constraints) they need to avoid false positives about intentional behavior and long-standing design decisions, and reduces duplicate code-tracing across the six specialist lenses.
1093
1220
 
1094
1221
  For small changes (a single file or trivial diff), you may do this research inline instead of dispatching a sub-agent. Use your judgment: if the workflow research would require reading more than 3-4 files, dispatch a sub-agent to preserve your context for orchestration.
1095
1222
 
@@ -1119,7 +1246,7 @@ For changes estimated at 3 points or fewer, skip partitioning — dispatch one s
1119
1246
 
1120
1247
  ## Step 6: Dispatch specialist sub-agents
1121
1248
 
1122
- For each review unit, dispatch sub-agents using the Task tool. Each sub-agent runs one specialist lens on one review unit. The five specialists are:
1249
+ For each review unit, dispatch sub-agents using the Task tool. Each sub-agent runs one specialist lens on one review unit. The six specialists are:
1123
1250
 
1124
1251
  1. **Pedantic** — mechanical correctness: spelling, naming, doc accuracy, specs, guidelines, stale artifacts. Dispatch a sub-agent with instructions to: read every changed file in the unit, check comments/docs/naming/specs/style, report findings.
1125
1252
 
@@ -1131,6 +1258,8 @@ For each review unit, dispatch sub-agents using the Task tool. Each sub-agent ru
1131
1258
 
1132
1259
  5. **Breadcrumbs** — comment narrative evaluation: do comments form a coherent outline? Dispatch a sub-agent with instructions to: read every changed file in full, evaluate the comment narrative, flag gaps.
1133
1260
 
1261
+ 6. **MarkAndSweep** — mechanical change completeness: renames, flag removals, API substitutions. Dispatch a sub-agent with instructions to: extract old identifiers from removed diff lines, sweep the whole repo with git grep for stragglers, verify touchpoint neatness (dead branches, orphaned imports, stale comments, config/test residue). Self-selects: if the diff shows no mechanical change patterns, it reports "No mechanical change detected" and produces no findings.
1262
+
1134
1263
  For the integration review unit (5+ points), dispatch a sub-agent focused on:
1135
1264
  - Cross-component contracts — do the interfaces between components match?
1136
1265
  - Boundary correctness — race conditions, ordering dependencies, shared state issues at boundaries.
@@ -1155,7 +1284,7 @@ Alternatively, perform the synthesis yourself:
1155
1284
  When dispatching each sub-agent, include in the prompt:
1156
1285
  - The git range to review
1157
1286
  - The specific files in this unit's scope
1158
- - The specialist focus (from the five specialists above)
1287
+ - The specialist focus (from the six specialists above)
1159
1288
  - The diff stat for this unit's files
1160
1289
  - **The project context brief** from Step 2, filtered to what is relevant to this unit's scope. Explicitly list any deferred work that falls within this unit's files, and call out TODO ($ticket) markers the specialists should recognize as intentional.
1161
1290
  - **The workflow guide** from Step 3, filtered to the workflows relevant to this unit's scope. Use it to understand the purpose and evolution of the code before flagging issues. It provides the code-level context (flows, contracts, history, constraints) that prevents false positives about intentional behavior and long-standing design decisions.
@@ -1180,6 +1309,7 @@ const SHARED_SKILLS: SkillDef[] = [
1180
1309
  { name: "thatch-review-state-flow", content: REVIEW_STATE_FLOW },
1181
1310
  { name: "thatch-review-no-slop", content: REVIEW_NO_SLOP },
1182
1311
  { name: "thatch-review-breadcrumbs", content: REVIEW_BREADCRUMBS },
1312
+ { name: "thatch-review-mark-and-sweep", content: REVIEW_MARK_AND_SWEEP },
1183
1313
  { name: "thatch-review-synthesizer", content: REVIEW_SYNTHESIZER },
1184
1314
  { name: "thatch-review-context", content: REVIEW_CONTEXT },
1185
1315
  { name: "thatch-workflow-research", content: WORKFLOW_RESEARCH },
package/src/tool-defs.ts CHANGED
@@ -40,6 +40,7 @@ function formatEntry(
40
40
  let meta = `[${entry.store}]`;
41
41
  if (entry.branch) meta += ` branch:${entry.branch}`;
42
42
  if (entry.confidence) meta += ` confidence:${entry.confidence}`;
43
+ if (entry.archived) meta += " archived:true";
43
44
  parts.push(meta, "", entry.content);
44
45
  return parts.join("\n");
45
46
  }
@@ -48,6 +49,7 @@ function formatRecallResult(entry: any): string {
48
49
  let meta = `store:${entry.store}`;
49
50
  if (entry.branch) meta += ` branch:${entry.branch}`;
50
51
  if (entry.confidence) meta += ` confidence:${entry.confidence}`;
52
+ if (entry.archived) meta += " archived:true";
51
53
  const score = entry._score.toFixed(3);
52
54
  return `[${meta}] [score:${score}]\n${entry.content}`;
53
55
  }
@@ -118,6 +120,11 @@ const rememberDef: ToolDef = {
118
120
  overwrite: z.boolean().optional().describe(
119
121
  "Set to true to replace an existing memory with the same label.",
120
122
  ),
123
+ archived: z.boolean().optional().describe(
124
+ "Mark this memory as a stable, long-term record that should not trigger hygiene nudges. " +
125
+ "When updating an already-archived memory, this param is REQUIRED — " +
126
+ "pass archived: true to keep it archived, or archived: false to unarchive.",
127
+ ),
121
128
  },
122
129
  async execute(args, ctx) {
123
130
  const store = (args.store as string) || ctx.defaultStore;
@@ -125,9 +132,6 @@ const rememberDef: ToolDef = {
125
132
  const content = `# ${label}\n\n${args.content as string}`;
126
133
  const embedding = await ctx.model.passageEmbed(content);
127
134
 
128
- // Write-time collision check: the embedding is already computed, so
129
- // scanning the store is nearly free. The save proceeds regardless —
130
- // the agent decides whether and how to reconcile.
131
135
  const similar = ctx.db.findSimilar(store, embedding, {
132
136
  excludeSlug: ctx.db.slugify(label),
133
137
  });
@@ -136,6 +140,7 @@ const rememberDef: ToolDef = {
136
140
  branch: args.branch as string | undefined,
137
141
  confidence: args.confidence as number | undefined,
138
142
  overwrite: args.overwrite as boolean | undefined,
143
+ archived: args.archived as boolean | undefined,
139
144
  });
140
145
 
141
146
  if (!result.ok) return result.error;
@@ -172,6 +177,9 @@ const recallDef: ToolDef = {
172
177
  limit: z.number().int().min(1).max(20).optional().describe(
173
178
  "Maximum number of results. Default 10.",
174
179
  ),
180
+ includeArchived: z.boolean().optional().describe(
181
+ "Include archived memories in results. Default false — archived memories are excluded.",
182
+ ),
175
183
  },
176
184
  async execute(args, ctx) {
177
185
  const stores = args.store
@@ -183,6 +191,7 @@ const recallDef: ToolDef = {
183
191
  const results = ctx.db.recall(stores, queryEmbedding, {
184
192
  branch: args.branch as string | undefined,
185
193
  limit,
194
+ includeArchived: args.includeArchived as boolean | undefined,
186
195
  });
187
196
 
188
197
  if (results.length === 0) return "No matching memories found.";
@@ -212,6 +221,7 @@ const listDef: ToolDef = {
212
221
  let line = `[${store}] ${e.label}`;
213
222
  if (e.branch) line += ` (branch:${e.branch})`;
214
223
  if (e.confidence) line += ` (c:${e.confidence})`;
224
+ if (e.archived) line += " (archived)";
215
225
  line += ` (${e.updated_at})`;
216
226
  return line;
217
227
  })