@jeffober/thatch 0.1.26 → 0.1.28

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 (72) hide show
  1. package/README.md +7 -7
  2. package/artifacts/skills/common.md +1 -1
  3. package/artifacts/skills/thatch-change-walkthrough.md +2 -2
  4. package/artifacts/skills/thatch-code-archaeology.md +252 -0
  5. package/artifacts/skills/thatch-code-review.md +4 -4
  6. package/artifacts/skills/thatch-code-walkthrough.md +2 -2
  7. package/artifacts/skills/thatch-coding-workflow.md +79 -0
  8. package/artifacts/skills/thatch-fact-extractor.md +2 -2
  9. package/artifacts/skills/{pr-description.md → thatch-pr-description.md} +1 -1
  10. package/artifacts/skills/thatch-review-context.md +9 -0
  11. package/artifacts/skills/thatch-review-followup.md +29 -1
  12. package/artifacts/skills/thatch-review-pedantic.md +16 -1
  13. package/artifacts/skills/thatch-review-response.md +365 -0
  14. package/artifacts/skills/thatch-review-synthesizer.md +92 -5
  15. package/artifacts/skills/{split-overlarge-pr.md → thatch-split-overlarge-pr.md} +1 -1
  16. package/artifacts/skills/{ticket-description.md → thatch-ticket-description.md} +3 -3
  17. package/bin/release +3 -3
  18. package/bin/thatch +11 -10
  19. package/docs/dev/README.md +7 -6
  20. package/docs/dev/mcp-parity.md +1 -1
  21. package/docs/dev/skills.md +29 -9
  22. package/docs/plans/README.md +9 -6
  23. package/docs/plans/intuition-drives.md +147 -0
  24. package/docs/plans/nomenclater.md +89 -0
  25. package/docs/plans/prediction-consolidation.md +361 -0
  26. package/docs/qa/README.md +20 -3
  27. package/docs/user/README.md +19 -5
  28. package/package.json +1 -1
  29. package/src/db.ts +6 -6
  30. package/src/embeddings.ts +2 -2
  31. package/src/extract-queue.ts +4 -4
  32. package/src/extraction.ts +4 -4
  33. package/src/git.ts +1 -1
  34. package/src/hygiene.ts +1 -1
  35. package/src/index.ts +40 -34
  36. package/src/mcp.ts +34 -6
  37. package/src/prompts.ts +96 -85
  38. package/src/setup.ts +19 -19
  39. package/src/sideband.ts +3 -3
  40. package/src/skills.ts +104 -6
  41. package/src/tool-defs.ts +93 -14
  42. package/src/tools.ts +12 -2
  43. package/artifacts/skills/thatch-workflow-research.md +0 -91
  44. package/docs/in-progress/prediction-engine.md +0 -404
  45. package/docs/plans/001-memory-store.md +0 -106
  46. package/docs/plans/002-agent-driven-pipelines.md +0 -59
  47. package/docs/plans/003-memory-hygiene.md +0 -61
  48. package/docs/qa/use-cases/README.md +0 -51
  49. package/docs/qa/use-cases/UC-001-memory-roundtrip.md +0 -20
  50. package/docs/qa/use-cases/UC-002-dedup-cycle.md +0 -23
  51. package/docs/qa/use-cases/UC-003-extraction-nudge.md +0 -56
  52. package/docs/qa/use-cases/UC-004-cli-inspection.md +0 -22
  53. package/docs/qa/use-cases/UC-005-setup-install.md +0 -34
  54. package/docs/qa/use-cases/UC-006-cursor-hook-lifecycle.md +0 -32
  55. package/docs/qa/use-cases/UC-007-recall-nudge.md +0 -31
  56. package/docs/qa/use-cases/UC-008-hygiene-heartbeat.md +0 -26
  57. package/docs/qa/use-cases/UC-009-flush-tools-tiers.md +0 -33
  58. package/docs/qa/use-cases/UC-010-prime.md +0 -25
  59. package/docs/qa/use-cases/UC-011-write-time-similarity-warning.md +0 -24
  60. package/docs/qa/use-cases/UC-012-model-migration.md +0 -28
  61. package/docs/qa/use-cases/UC-013-compaction-context.md +0 -25
  62. package/docs/qa/use-cases/UC-014-skill-install-drift.md +0 -28
  63. package/docs/qa/use-cases/UC-015-env-override-matrix.md +0 -28
  64. package/docs/qa/use-cases/UC-016-concurrent-session-isolation.md +0 -37
  65. package/docs/qa/use-cases/UC-017-buffer-tool-vs-buffer-batch.md +0 -27
  66. package/docs/qa/use-cases/UC-018-mcp-startup-setup-detection.md +0 -38
  67. package/docs/qa/use-cases/UC-019-archived-memory-lifecycle.md +0 -36
  68. package/docs/qa/use-cases/UC-020-extraction-escalation.md +0 -52
  69. package/docs/qa/use-cases/UC-021-prediction-autofire.md +0 -21
  70. package/docs/qa/use-cases/UC-022-prediction-dedup.md +0 -19
  71. package/docs/qa/use-cases/UC-023-prediction-confidence-model.md +0 -23
  72. package/docs/qa/use-cases/UC-024-prediction-delete.md +0 -20
package/src/index.ts CHANGED
@@ -19,7 +19,7 @@ import { installSkills, SHARED_SKILLS, OPENCODE_ONLY_SKILLS } from "./skills";
19
19
  import { hygieneReport } from "./hygiene";
20
20
 
21
21
  // ---------------------------------------------------------------------------
22
- // V1 server export — tools, prompt injection, session hooks
22
+ // V1 server export - tools, prompt injection, session hooks
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
25
  // Minimum cosine score for the prompt-aware recall nudge. Lower than
@@ -27,7 +27,7 @@ import { hygieneReport } from "./hygiene";
27
27
  // than "duplicate." Tunable via THATCH_RECALL_THRESHOLD.
28
28
  const RECALL_THRESHOLD = parseFloat(process.env.THATCH_RECALL_THRESHOLD ?? "0.55");
29
29
 
30
- // Prompts shorter than this skip the recall nudge — trivially short prompts
30
+ // Prompts shorter than this skip the recall nudge - trivially short prompts
31
31
  // like "yes" or "ok" match too broadly to be useful.
32
32
  const MIN_PROMPT_LEN = 10;
33
33
 
@@ -50,12 +50,12 @@ export const server: Plugin = async ({ client, worktree }) => {
50
50
  const extraction = new ExtractionPipeline();
51
51
 
52
52
  // Sessions currently being compacted. chat.message nudges are skipped while
53
- // a session is in this set — the agent can't call tools during summary
53
+ // a session is in this set - the agent can't call tools during summary
54
54
  // generation, so a recall or extraction nudge would cause a blocked-tool
55
55
  // error. Cleared by experimental.compaction.autocontinue (success), the
56
56
  // session.compacted event (redundant belt-and-suspenders), or chat.message
57
57
  // itself when a non-compaction message arrives (compaction failure fallback
58
- // — autocontinue never fired, so the next real user message clears the
58
+ // - autocontinue never fired, so the next real user message clears the
59
59
  // stale flag). If all three somehow miss, the flag leaks (graceful
60
60
  // degradation: nudges stay off for that session, but no crash).
61
61
  const compacting = new Set<string>();
@@ -78,7 +78,7 @@ export const server: Plugin = async ({ client, worktree }) => {
78
78
  // When the child writes a memory, consumeSnapshot drains only these entries
79
79
  // (by reference identity), preserving interleaved-turn entries that arrived
80
80
  // while the sub-agent was running. Without this, the child's memory_remember
81
- // would drain the parent's ENTIRE buffer — silently dropping facts from
81
+ // would drain the parent's ENTIRE buffer - silently dropping facts from
82
82
  // any tool calls the parent made concurrently with the sub-agent.
83
83
  const parentSnapshots = new Map<string, ToolInteraction[]>();
84
84
 
@@ -108,7 +108,7 @@ export const server: Plugin = async ({ client, worktree }) => {
108
108
  // Keyed by child session ID. Cleaned up on child idle, error, or deletion.
109
109
  const childMetrics = new Map<string, { new: number; updated: number; deleted: number }>();
110
110
 
111
- // Skills always install to the global opencode config — installing into the
111
+ // Skills always install to the global opencode config - installing into the
112
112
  // worktree would mutate the user's repo (untracked files in git status).
113
113
  // A failed install degrades the nudge workflow but must not kill the plugin.
114
114
  try {
@@ -129,7 +129,7 @@ export const server: Plugin = async ({ client, worktree }) => {
129
129
  // drain machinery handles buffer cleanup. The nudge path is a fallback if
130
130
  // this throws.
131
131
  //
132
- // Does NOT call extraction.accept — entries stay in pending so consumeSnapshot
132
+ // Does NOT call extraction.accept - entries stay in pending so consumeSnapshot
133
133
  // can drain them by reference identity when the child writes a memory. The
134
134
  // extracting set (not accept) suppresses the nudge in chat.message.
135
135
  //
@@ -141,8 +141,7 @@ export const server: Plugin = async ({ client, worktree }) => {
141
141
  extracting.add(parentID);
142
142
 
143
143
  const batch = extraction.peek(parentID);
144
- const payload = extraction.buildPayload(batch, repo);
145
- const promptText = extractionDirectPrompt(batch.length, payload);
144
+ const promptText = extractionDirectPrompt(batch.length, parentID);
146
145
 
147
146
  const result = await client.session.create({
148
147
  body: { parentID, title: "thatch-extraction" },
@@ -183,7 +182,7 @@ export const server: Plugin = async ({ client, worktree }) => {
183
182
  throw err;
184
183
  }
185
184
  } else {
186
- // Fire and forget — the parent is already idle, so blocking the event
185
+ // Fire and forget - the parent is already idle, so blocking the event
187
186
  // handler would only delay other event processing. The child runs to
188
187
  // completion and its idle event triggers cleanup.
189
188
  client.session
@@ -201,14 +200,22 @@ export const server: Plugin = async ({ client, worktree }) => {
201
200
  }
202
201
 
203
202
  return {
204
- tool: createTools(db, model, repo),
205
-
206
- // 1. System prompt — always in context.
203
+ tool: createTools(db, model, repo, {
204
+ extractionPayloadProvider: (sessionID: string): string | null => {
205
+ const interactions = extraction.peek(sessionID);
206
+ const accepted = extraction.peekAccepted(sessionID);
207
+ const all = [...accepted, ...interactions];
208
+ if (all.length === 0) return null;
209
+ return extraction.buildPayload(all, repo);
210
+ },
211
+ }),
212
+
213
+ // 1. System prompt - always in context.
207
214
  "experimental.chat.system.transform": async (_input, output) => {
208
215
  output.system.push(sys);
209
216
  },
210
217
 
211
- // 2. Compaction context — re-familiarizes after compaction. The flag
218
+ // 2. Compaction context - re-familiarizes after compaction. The flag
212
219
  // suppresses chat.message nudges during summary generation (tool
213
220
  // calls are blocked there).
214
221
  "experimental.session.compacting": async (input, output) => {
@@ -222,15 +229,15 @@ export const server: Plugin = async ({ client, worktree }) => {
222
229
  compacting.delete(input.sessionID);
223
230
  },
224
231
 
225
- // 3. Tool buffering — feeds the extraction pipeline (direct extraction
232
+ // 3. Tool buffering - feeds the extraction pipeline (direct extraction
226
233
  // via SDK or nudge fallback). Excluded tools:
227
234
  // - thatch_*: extracting facts from memory ops would echo the store
228
235
  // - skill/task: meta-tools that orchestrate agent behavior (loading
229
236
  // skills, dispatching sub-agents). Buffering them creates a feedback
230
- // loop — extraction triggers a skill load, which gets buffered, which
237
+ // loop - extraction triggers a skill load, which gets buffered, which
231
238
  // triggers another extraction on the next turn.
232
239
  // Memory writes consume the buffer and reset the missed-nudge counter.
233
- // The buffer is NOT drained on nudge delivery — it persists until the
240
+ // The buffer is NOT drained on nudge delivery - it persists until the
234
241
  // agent writes a memory, so ignored nudges accumulate and the payload
235
242
  // grows with each missed cycle.
236
243
  //
@@ -247,7 +254,7 @@ export const server: Plugin = async ({ client, worktree }) => {
247
254
  // session errors or is deleted before completing.
248
255
  //
249
256
  // A memory_remember call in a child session (sub-agent) also drains
250
- // the parent's pending buffer — but only the entries that existed at
257
+ // the parent's pending buffer - but only the entries that existed at
251
258
  // dispatch time (the snapshot). Entries from interleaved turns survive
252
259
  // so their facts aren't silently dropped.
253
260
  "tool.execute.after": async (input, output) => {
@@ -269,7 +276,7 @@ export const server: Plugin = async ({ client, worktree }) => {
269
276
  // from the pending buffer. If no snapshot was recorded (unreachable
270
277
  // when childToParent has the entry, since both are set together in
271
278
  // session.created), skip the drain rather than dropping the entire
272
- // buffer — interleaved-turn entries that arrived while the child
279
+ // buffer - interleaved-turn entries that arrived while the child
273
280
  // was running must survive for the next extraction cycle.
274
281
  extraction.completeAccepted(parentID);
275
282
  const snapshot = parentSnapshots.get(input.sessionID);
@@ -300,7 +307,7 @@ export const server: Plugin = async ({ client, worktree }) => {
300
307
  return;
301
308
  }
302
309
  // Track memory deletions in child sessions for the toast metrics.
303
- // Same scoping as the remember handler above — only child sessions,
310
+ // Same scoping as the remember handler above - only child sessions,
304
311
  // not the parent or manual memory writes from the user's session.
305
312
  if (input.tool === "thatch_memory_forget" && childToParent.has(input.sessionID)) {
306
313
  const metrics = childMetrics.get(input.sessionID) ?? { new: 0, updated: 0, deleted: 0 };
@@ -318,7 +325,7 @@ export const server: Plugin = async ({ client, worktree }) => {
318
325
  });
319
326
  },
320
327
 
321
- // 4. Per-message nudge — two priority tiers:
328
+ // 4. Per-message nudge - two priority tiers:
322
329
  // a. Extraction nudge: prior tool interactions are queued for fact
323
330
  // extraction (carries the JSON payload for thatch-fact-extractor).
324
331
  // b. Recall + prediction nudge: when no extraction is pending, embed
@@ -337,24 +344,23 @@ export const server: Plugin = async ({ client, worktree }) => {
337
344
  if (compacting.has(input.sessionID)) {
338
345
  // The session is marked as compacting. If this message is the
339
346
  // compaction summary generation itself (has a compaction-type part),
340
- // suppress nudges — tools are blocked during summary generation and a
347
+ // suppress nudges - tools are blocked during summary generation and a
341
348
  // nudge would cause a blocked-tool error. If it is NOT a compaction
342
349
  // message, compaction has already failed (autocontinue never fired)
343
350
  // and the user is sending a new message. Clear the stale flag and
344
- // proceed normally — tools are available again.
351
+ // proceed normally - tools are available again.
345
352
  const isCompactionMsg = (output.parts as any[]).some((p) => p.type === "compaction");
346
353
  if (isCompactionMsg) return;
347
354
  compacting.delete(input.sessionID);
348
355
  }
349
356
  // Extraction nudge (fallback path). Skipped when the extracting set
350
- // is active — that means a direct-extraction child session is running
357
+ // is active - that means a direct-extraction child session is running
351
358
  // and the plugin is handling extraction via the SDK. The nudge fires
352
359
  // here only when direct extraction was never triggered or threw.
353
360
  if (!extracting.has(input.sessionID) && extraction.pending(input.sessionID)) {
354
361
  const batch = extraction.peek(input.sessionID);
355
- const payload = extraction.buildPayload(batch, repo);
356
362
  const missed = missedNudges.get(input.sessionID) ?? 0;
357
- const text = extractionNudge(batch.length, missed, "thatch_memory_remember", payload);
363
+ const text = extractionNudge(batch.length, missed, "thatch_memory_remember", input.sessionID);
358
364
  missedNudges.set(input.sessionID, missed + 1);
359
365
 
360
366
  output.parts.push({
@@ -368,7 +374,7 @@ export const server: Plugin = async ({ client, worktree }) => {
368
374
  return;
369
375
  }
370
376
 
371
- // No extraction pending — try the prompt-aware recall nudge. Extract
377
+ // No extraction pending - try the prompt-aware recall nudge. Extract
372
378
  // the user's prompt text from the message parts, embed it with the
373
379
  // warm in-process model, and search for matches. Best-effort: any
374
380
  // failure (no text, model not loaded, empty store) silently skips.
@@ -469,7 +475,7 @@ export const server: Plugin = async ({ client, worktree }) => {
469
475
  if (info.parentID) {
470
476
  childToParent.set(info.id, info.parentID);
471
477
  parentSnapshots.set(info.id, [...extraction.peek(info.parentID)]);
472
- // Child sessions don't need the session-start reminder — only
478
+ // Child sessions don't need the session-start reminder - only
473
479
  // top-level sessions do. Returning here prevents extraction
474
480
  // children from receiving the hygiene report and tool overview.
475
481
  return;
@@ -499,23 +505,23 @@ export const server: Plugin = async ({ client, worktree }) => {
499
505
  // - Task-dispatched sub-agent (code review specialist, nudge-path
500
506
  // fact-extractor, any model-dispatched task): complete the
501
507
  // parent's accepted entries and reset missedNudges. Do NOT drain
502
- // the buffer or delete the session — the task tool that
508
+ // the buffer or delete the session - the task tool that
503
509
  // dispatched the sub-agent needs to read its output.
504
510
  if (extractionChildren.has(sessionID)) {
505
511
  // Drain the parent's snapshot entries from the pending buffer.
506
512
  // If the child wrote memories, consumeSnapshot already ran in
507
- // tool.execute.after and the snapshot is gone — nothing to
513
+ // tool.execute.after and the snapshot is gone - nothing to
508
514
  // drain. If the child did a no-save run, the snapshot entries
509
515
  // are still in the buffer and need to be drained here so they
510
516
  // don't replay as a nudge on the next chat.message. Never drain
511
- // the entire buffer — interleaved-turn entries must survive.
517
+ // the entire buffer - interleaved-turn entries must survive.
512
518
  const snapshot = parentSnapshots.get(sessionID);
513
519
  if (snapshot) {
514
520
  extraction.consumeSnapshot(parentID, snapshot);
515
521
  }
516
522
 
517
523
  // Fire a toast with the extraction metrics. Only show a toast
518
- // when memories were actually written — no toast for no-save
524
+ // when memories were actually written - no toast for no-save
519
525
  // runs to avoid notification fatigue.
520
526
  const metrics = childMetrics.get(sessionID);
521
527
  const parts: string[] = [];
@@ -546,7 +552,7 @@ export const server: Plugin = async ({ client, worktree }) => {
546
552
  try {
547
553
  await client.session.delete({ path: { id: sessionID } });
548
554
  } catch {
549
- // Best-effort — the child is idle and harmless if not deleted.
555
+ // Best-effort - the child is idle and harmless if not deleted.
550
556
  }
551
557
  } else {
552
558
  // Task-dispatched sub-agent went idle. Complete the parent's
@@ -557,7 +563,7 @@ export const server: Plugin = async ({ client, worktree }) => {
557
563
  }
558
564
  return;
559
565
  }
560
- // Parent went idle — trigger direct extraction if there are pending
566
+ // Parent went idle - trigger direct extraction if there are pending
561
567
  // tool interactions and no extraction is already running. Falls
562
568
  // back to the nudge path on the next chat.message if this throws.
563
569
  if (
package/src/mcp.ts CHANGED
@@ -5,6 +5,8 @@ import { detectRepo } from "./git";
5
5
  import { checkSetup } from "./setup";
6
6
  import { TOOL_DEFS, type CoreContext, type ToolDef } from "./tool-defs";
7
7
  import { SidebandServer, sidebandSocketPath } from "./sideband";
8
+ import { peekQueue, consumeQueue, resetMissedCount } from "./extract-queue";
9
+ import { buildExtractionPayload } from "./extraction";
8
10
  import pkg from "../package.json";
9
11
 
10
12
  // ---------------------------------------------------------------------------
@@ -39,14 +41,14 @@ const SERVER_VERSION = pkg.version;
39
41
 
40
42
  /**
41
43
  * MCP capabilities declared in the initialize response. Thatch is a tools-only
42
- * server — no resources, prompts, or subscriptions.
44
+ * server - no resources, prompts, or subscriptions.
43
45
  */
44
46
  const CAPABILITIES = {
45
47
  tools: { listChanged: false },
46
48
  };
47
49
 
48
50
  // ---------------------------------------------------------------------------
49
- // Tool index — builds validators and JSON Schema once at server startup
51
+ // Tool index - builds validators and JSON Schema once at server startup
50
52
  // ---------------------------------------------------------------------------
51
53
 
52
54
  interface CompiledTool {
@@ -74,7 +76,7 @@ function compileTools(): Map<string, CompiledTool> {
74
76
 
75
77
  /**
76
78
  * Runs the thatch MCP server over stdio. Reads newline-delimited JSON-RPC from
77
- * stdin, writes responses to stdout. All diagnostics go to stderr — any stray
79
+ * stdin, writes responses to stdout. All diagnostics go to stderr - any stray
78
80
  * output on stdout corrupts the protocol.
79
81
  *
80
82
  * Repo identity is resolved from CLAUDE_PROJECT_DIR (set by Claude Code for
@@ -98,7 +100,33 @@ export async function runMcpServer(): Promise<void> {
98
100
 
99
101
  const db = new ThatchDB(dbPath);
100
102
  const model = new BgeEmbeddingModel(modelName);
101
- const ctx: CoreContext = { db, model, defaultStore: repo };
103
+
104
+ // Extraction payload provider: peeks the file-backed queue and returns the
105
+ // serialized JSON payload. Called by the get_extraction_payload tool when a
106
+ // sub-agent needs to fetch the parent's queued interactions. Returns null
107
+ // when no interactions are queued.
108
+ const extractionPayloadProvider = (sessionID: string): string | null => {
109
+ const interactions = peekQueue(sessionID);
110
+ if (interactions.length === 0) return null;
111
+ return buildExtractionPayload(interactions, repo);
112
+ };
113
+
114
+ // Drain a session's file-backed queue and reset its missed-nudge counter.
115
+ // Called by extraction_done when a sub-agent passes the parent's session_id.
116
+ // Without this, the MCP path's appendBatch self-detection would drain the
117
+ // sub-agent's (empty) queue instead of the parent's.
118
+ const drainExtractionQueue = (sessionID: string): void => {
119
+ resetMissedCount(sessionID);
120
+ consumeQueue(sessionID);
121
+ };
122
+
123
+ const ctx: CoreContext = {
124
+ db,
125
+ model,
126
+ defaultStore: repo,
127
+ extractionPayloadProvider,
128
+ drainExtractionQueue,
129
+ };
102
130
  const tools = compileTools();
103
131
 
104
132
  // Check whether `thatch setup` was run for the current host. If not, or if
@@ -113,7 +141,7 @@ export async function runMcpServer(): Promise<void> {
113
141
  }
114
142
 
115
143
  // The sideband socket lets one-shot hook processes (flush-tools) ask the
116
- // warm MCP server to embed a prompt and search for matches — without
144
+ // warm MCP server to embed a prompt and search for matches - without
117
145
  // loading the ~34 MB model themselves. If the socket can't be opened
118
146
  // (permissions, tmpdir issues), the MCP server still works; only the
119
147
  // prompt-aware recall nudge degrades.
@@ -146,7 +174,7 @@ export async function runMcpServer(): Promise<void> {
146
174
  continue;
147
175
  }
148
176
 
149
- // Notifications (no id) are fire-and-forget — no response expected.
177
+ // Notifications (no id) are fire-and-forget - no response expected.
150
178
  const isNotification = req.id === null || req.id === undefined;
151
179
  const res = await dispatch(req, tools, ctx);
152
180
  if (res === null || isNotification) continue;