@jeffober/thatch 0.1.18 → 0.1.20

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.
package/src/prompts.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { PredictionNudgeItem } from "./db";
2
+
1
3
  export function systemPrompt(repo: string): string {
2
4
  return `# Persistence
3
5
 
@@ -7,7 +9,9 @@ knowledge so future sessions can build on what you've already learned.
7
9
  Tools: thatch_memory_remember, thatch_memory_recall, thatch_memory_list,
8
10
  thatch_memory_show, thatch_memory_forget, thatch_store_list,
9
11
  thatch_find_duplicates, thatch_dedup_mark_checked,
10
- thatch_extraction_done
12
+ thatch_extraction_done,
13
+ thatch_prediction_query, thatch_prediction_update, thatch_prediction_list,
14
+ thatch_prediction_delete
11
15
 
12
16
  ## Stores
13
17
 
@@ -42,8 +46,8 @@ workflows. The host auto-discovers them, but reach for them proactively:
42
46
  - \`thatch-session-reflection\` — record what you learned at end of session.
43
47
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
44
48
  - \`thatch-dedup-classifier\` — resolve duplicate memory pairs from \`thatch_find_duplicates\`.
45
- - \`pr-description\` — draft a PR description using instructional-design scaffolding with bold and italic emphasis on the phrases that carry the meaning, so a reviewer skimming only the emphasized fragments still gets the story.
46
- - \`ticket-description\` — draft a ticket, issue, or work item (Linear or Jira) using instructional-design scaffolding with bold and italic emphasis for scanning.
49
+ - \`pr-description\` — draft a PR description with SYNOPSIS / PURPOSE / DESCRIPTION / WALK-THROUGH / NOTES, using bold and italic emphasis on the phrases that carry the meaning.
50
+ - \`ticket-description\` — draft a ticket, issue, or work item (Linear or Jira) with clear sections and bold/italic emphasis for scanning.
47
51
  - \`split-overlarge-pr\` — split already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main.
48
52
 
49
53
  ## When to Write
@@ -60,6 +64,35 @@ whether you've discovered knowledge worth persisting. Use thatch_memory_recall
60
64
  to check for duplicates, then thatch_memory_remember for new findings. Then
61
65
  deliver your response.
62
66
 
67
+ ## User Decision Model
68
+
69
+ A statistical model of the user's decision-making preferences is maintained by
70
+ the prediction engine. Scored predictions may appear above as "User decision
71
+ model" context; these are learned patterns about what the user tends to
72
+ prefer in specific situations.
73
+
74
+ When facing a judgment call about scope, appropriateness, or methodology:
75
+ - If a prediction is strong (high confidence, sufficient evidence), follow it
76
+ - If predictions conflict or confidence is thin, surface the uncertainty
77
+ naturally: "I think you usually prefer X here, but I'm not sure; what do
78
+ you want?"
79
+ - When the user responds to a surfaced prediction, use thatch_prediction_update
80
+ to reinforce or weaken the model
81
+
82
+ When the user corrects you, answers your question, or provides a clear signal
83
+ about their preferences:
84
+ 1. Use thatch_prediction_query to check for existing matchers and predictions
85
+ 2. Use thatch_prediction_update to create, reinforce, or weaken a prediction
86
+ 3. Use thatch_prediction_delete to remove a prediction created in error
87
+
88
+ Code review discussions are high-signal prediction material. When working
89
+ through potential review comments with the user, watch for preferences about
90
+ review threshold, severity, tone, evidence, scope, false positives, or what is
91
+ worth posting. This applies whether the user invoked a review skill, is
92
+ reviewing your changes, or asked you to review their changes or another LLM's
93
+ changes. Before creating a new prediction, query for an existing one and
94
+ reinforce or adjust it when possible.
95
+
63
96
  ## What to Store
64
97
 
65
98
  - **Global store**: user observations, agent personality, system environment
@@ -150,8 +183,9 @@ Tools are prefixed in Claude Code: \`mcp__thatch__memory_remember\`,
150
183
  \`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
151
184
  \`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
152
185
  \`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
153
- \`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`.
154
- Bare names used below for readability.
186
+ \`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`,
187
+ \`mcp__thatch__prediction_query\`, \`mcp__thatch__prediction_update\`,
188
+ \`mcp__thatch__prediction_list\`, \`mcp__thatch__prediction_delete\`. Bare names used below for readability.
155
189
 
156
190
  ## Stores
157
191
 
@@ -186,8 +220,8 @@ workflows. The host auto-discovers them, but reach for them proactively:
186
220
  - \`thatch-session-reflection\` — record what you learned at end of session.
187
221
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
188
222
  - \`thatch-dedup-classifier\` — resolve duplicate memory pairs from \`find_duplicates\`.
189
- - \`pr-description\` — draft a PR description using instructional-design scaffolding with bold and italic emphasis on the phrases that carry the meaning, so a reviewer skimming only the emphasized fragments still gets the story.
190
- - \`ticket-description\` — draft a ticket, issue, or work item (Linear or Jira) using instructional-design scaffolding with bold and italic emphasis for scanning.
223
+ - \`pr-description\` — draft a PR description with SYNOPSIS / PURPOSE / DESCRIPTION / WALK-THROUGH / NOTES, using bold and italic emphasis on the phrases that carry the meaning.
224
+ - \`ticket-description\` — draft a ticket, issue, or work item (Linear or Jira) with clear sections and bold/italic emphasis for scanning.
191
225
  - \`split-overlarge-pr\` — split already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main.
192
226
 
193
227
  ## When to Write
@@ -204,6 +238,35 @@ whether you've discovered knowledge worth persisting. Use memory_recall to
204
238
  check for duplicates, then memory_remember for new findings. Then deliver
205
239
  your response.
206
240
 
241
+ ## User Decision Model
242
+
243
+ A statistical model of the user's decision-making preferences is maintained by
244
+ the prediction engine. Scored predictions may appear above as "User decision
245
+ model" context; these are learned patterns about what the user tends to
246
+ prefer in specific situations.
247
+
248
+ When facing a judgment call about scope, appropriateness, or methodology:
249
+ - If a prediction is strong (high confidence, sufficient evidence), follow it
250
+ - If predictions conflict or confidence is thin, surface the uncertainty
251
+ naturally: "I think you usually prefer X here, but I'm not sure; what do
252
+ you want?"
253
+ - When the user responds to a surfaced prediction, use prediction_update
254
+ to reinforce or weaken the model
255
+
256
+ When the user corrects you, answers your question, or provides a clear signal
257
+ about their preferences:
258
+ 1. Use prediction_query to check for existing matchers and predictions
259
+ 2. Use prediction_update to create, reinforce, or weaken a prediction
260
+ 3. Use prediction_delete to remove a prediction created in error
261
+
262
+ Code review discussions are high-signal prediction material. When working
263
+ through potential review comments with the user, watch for preferences about
264
+ review threshold, severity, tone, evidence, scope, false positives, or what is
265
+ worth posting. This applies whether the user invoked a review skill, is
266
+ reviewing your changes, or asked you to review their changes or another LLM's
267
+ changes. Before creating a new prediction, query for an existing one and
268
+ reinforce or adjust it when possible.
269
+
207
270
  ## What to Store
208
271
 
209
272
  - **Global store**: user observations, agent personality, system environment
@@ -356,8 +419,9 @@ Tools are prefixed in Cursor: \`mcp__thatch__memory_remember\`,
356
419
  \`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
357
420
  \`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
358
421
  \`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
359
- \`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`.
360
- Bare names used below for readability.
422
+ \`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`,
423
+ \`mcp__thatch__prediction_query\`, \`mcp__thatch__prediction_update\`,
424
+ \`mcp__thatch__prediction_list\`, \`mcp__thatch__prediction_delete\`. Bare names used below for readability.
361
425
 
362
426
  ## Stores
363
427
 
@@ -392,8 +456,8 @@ workflows. The host auto-discovers them, but reach for them proactively:
392
456
  - \`thatch-session-reflection\` — record what you learned at end of session.
393
457
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
394
458
  - \`thatch-dedup-classifier\` — resolve duplicate memory pairs from \`find_duplicates\`.
395
- - \`pr-description\` — draft a PR description using instructional-design scaffolding with bold and italic emphasis on the phrases that carry the meaning, so a reviewer skimming only the emphasized fragments still gets the story.
396
- - \`ticket-description\` — draft a ticket, issue, or work item (Linear or Jira) using instructional-design scaffolding with bold and italic emphasis for scanning.
459
+ - \`pr-description\` — draft a PR description with SYNOPSIS / PURPOSE / DESCRIPTION / WALK-THROUGH / NOTES, using bold and italic emphasis on the phrases that carry the meaning.
460
+ - \`ticket-description\` — draft a ticket, issue, or work item (Linear or Jira) with clear sections and bold/italic emphasis for scanning.
397
461
  - \`split-overlarge-pr\` — split already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main.
398
462
 
399
463
  ## When to Write
@@ -410,6 +474,35 @@ whether you've discovered knowledge worth persisting. Use memory_recall to
410
474
  check for duplicates, then memory_remember for new findings. Then deliver
411
475
  your response.
412
476
 
477
+ ## User Decision Model
478
+
479
+ A statistical model of the user's decision-making preferences is maintained by
480
+ the prediction engine. Scored predictions may appear above as "User decision
481
+ model" context; these are learned patterns about what the user tends to
482
+ prefer in specific situations.
483
+
484
+ When facing a judgment call about scope, appropriateness, or methodology:
485
+ - If a prediction is strong (high confidence, sufficient evidence), follow it
486
+ - If predictions conflict or confidence is thin, surface the uncertainty
487
+ naturally: "I think you usually prefer X here, but I'm not sure; what do
488
+ you want?"
489
+ - When the user responds to a surfaced prediction, use prediction_update
490
+ to reinforce or weaken the model
491
+
492
+ When the user corrects you, answers your question, or provides a clear signal
493
+ about their preferences:
494
+ 1. Use prediction_query to check for existing matchers and predictions
495
+ 2. Use prediction_update to create, reinforce, or weaken a prediction
496
+ 3. Use prediction_delete to remove a prediction created in error
497
+
498
+ Code review discussions are high-signal prediction material. When working
499
+ through potential review comments with the user, watch for preferences about
500
+ review threshold, severity, tone, evidence, scope, false positives, or what is
501
+ worth posting. This applies whether the user invoked a review skill, is
502
+ reviewing your changes, or asked you to review their changes or another LLM's
503
+ changes. Before creating a new prediction, query for an existing one and
504
+ reinforce or adjust it when possible.
505
+
413
506
  ## What to Store
414
507
 
415
508
  - **Global store**: user observations, agent personality, system environment
@@ -495,3 +588,18 @@ function formatRecallNudge(matches: NudgeMatch[], toolName: string): string {
495
588
  const etc = n > 2 ? ", etc." : "";
496
589
  return `[thatch] ${n} ${word} ${verb} to this prompt (${labels}${etc}). Use ${toolName} before responding.`;
497
590
  }
591
+
592
+ // ---------------------------------------------------------------------------
593
+ // Prediction nudge: auto-injected when matchers fire for the user's prompt.
594
+ // Descriptive, not directive: the system prompt instructions govern how to
595
+ // act on the scored predictions (follow strong, surface weak/competing,
596
+ // update on user response).
597
+ // ---------------------------------------------------------------------------
598
+
599
+ export function predictionNudge(items: PredictionNudgeItem[]): string {
600
+ const lines = items.map((p) => {
601
+ const verb = p.evidence_count === 0 ? "you may prefer" : "you tend to";
602
+ return `- [${p.confidence.toFixed(2)} conf, ${p.evidence_count} tests] When ${p.matcher_description}: ${verb} ${p.statement}`;
603
+ });
604
+ return `[thatch] User decision model\n${lines.join("\n")}`;
605
+ }
package/src/sideband.ts CHANGED
@@ -3,7 +3,7 @@ import { unlinkSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { createServer, connect, type Server, type Socket } from "node:net";
6
- import type { ThatchDB } from "./db";
6
+ import type { ThatchDB, PredictionNudgeItem } from "./db";
7
7
  import type { EmbeddingModel } from "./embeddings";
8
8
 
9
9
  /**
@@ -24,7 +24,7 @@ export interface SidebandMatch {
24
24
  }
25
25
 
26
26
  interface MatchRequest {
27
- method: "match";
27
+ method: "match" | "predictions";
28
28
  text: string;
29
29
  stores: string[];
30
30
  threshold: number;
@@ -34,6 +34,7 @@ interface MatchRequest {
34
34
  interface MatchResponse {
35
35
  ok: boolean;
36
36
  matches?: SidebandMatch[];
37
+ predictions?: PredictionNudgeItem[];
37
38
  error?: string;
38
39
  }
39
40
 
@@ -115,13 +116,22 @@ export class SidebandServer {
115
116
  return;
116
117
  }
117
118
 
118
- if (req.method !== "match") {
119
+ if (req.method !== "match" && req.method !== "predictions") {
119
120
  this.#respond(socket, { ok: false, error: `Unknown method: ${req.method}` });
120
121
  return;
121
122
  }
122
123
 
123
124
  try {
124
125
  const embedding = await this.#model.queryEmbed(req.text);
126
+
127
+ if (req.method === "predictions") {
128
+ const predictions = this.#db.scorePredictionNudge(
129
+ req.stores, embedding, req.threshold, 5,
130
+ );
131
+ this.#respond(socket, { ok: true, predictions });
132
+ return;
133
+ }
134
+
125
135
  const results = this.#db.search(req.stores, embedding, { limit: req.limit });
126
136
  const matches: SidebandMatch[] = results
127
137
  .filter((r) => r._score >= req.threshold)
@@ -156,16 +166,45 @@ export function sidebandMatch(
156
166
  limit: number,
157
167
  timeoutMs = 2000,
158
168
  ): Promise<SidebandMatch[] | null> {
169
+ return sidebandRequest("match", socketPath, text, stores, threshold, limit, timeoutMs)
170
+ .then((res) => res?.matches ?? null);
171
+ }
172
+
173
+ /**
174
+ * Connect to the sideband server and request scored predictions for a prompt.
175
+ * Returns null on any failure; callers treat null as "skip the prediction nudge."
176
+ */
177
+ export function sidebandPredictions(
178
+ socketPath: string,
179
+ text: string,
180
+ stores: string[],
181
+ threshold: number,
182
+ limit: number,
183
+ timeoutMs = 2000,
184
+ ): Promise<PredictionNudgeItem[] | null> {
185
+ return sidebandRequest("predictions", socketPath, text, stores, threshold, limit, timeoutMs)
186
+ .then((res) => res?.predictions ?? null);
187
+ }
188
+
189
+ function sidebandRequest(
190
+ method: "match" | "predictions",
191
+ socketPath: string,
192
+ text: string,
193
+ stores: string[],
194
+ threshold: number,
195
+ limit: number,
196
+ timeoutMs: number,
197
+ ): Promise<MatchResponse | null> {
159
198
  return new Promise((resolve) => {
160
199
  let settled = false;
161
- const done = (val: SidebandMatch[] | null) => {
200
+ const done = (val: MatchResponse | null) => {
162
201
  if (settled) return;
163
202
  settled = true;
164
203
  resolve(val);
165
204
  };
166
205
 
167
206
  const socket = connect(socketPath);
168
- const req = JSON.stringify({ method: "match", text, stores, threshold, limit }) + "\n";
207
+ const req = JSON.stringify({ method, text, stores, threshold, limit }) + "\n";
169
208
  let buf = "";
170
209
 
171
210
  const timer = setTimeout(() => {
@@ -173,14 +212,13 @@ export function sidebandMatch(
173
212
  done(null);
174
213
  }, timeoutMs);
175
214
 
176
- socket.on("error", () => {
215
+ socket.on("error", (err: NodeJS.ErrnoException) => {
177
216
  clearTimeout(timer);
178
- // Stale socket file left by a crashed server — clean it up so the next
179
- // connection attempt doesn't hit ECONNREFUSED again.
180
- try {
181
- unlinkSync(socketPath);
182
- } catch {
183
- // Already gone or never existed.
217
+ // Only remove the socket file if it's a stale-file situation
218
+ // (ECONNREFUSED). Other errors (timeout, server error) should
219
+ // not delete a live server's socket.
220
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
221
+ try { unlinkSync(socketPath); } catch { /* already gone */ }
184
222
  }
185
223
  done(null);
186
224
  });
@@ -197,7 +235,7 @@ export function sidebandMatch(
197
235
  clearTimeout(timer);
198
236
  try {
199
237
  const res = JSON.parse(buf.slice(0, nl)) as MatchResponse;
200
- done(res.ok ? (res.matches ?? []) : null);
238
+ done(res);
201
239
  } catch {
202
240
  done(null);
203
241
  }
package/src/tool-defs.ts CHANGED
@@ -2,6 +2,17 @@ import { z } from "zod";
2
2
  import type { ThatchDB, DedupCandidate } from "./db";
3
3
  import type { EmbeddingModel } from "./embeddings";
4
4
 
5
+ // Near-duplicate thresholds for matcher/prediction dedup at creation time.
6
+ // Matches the thatch_find_duplicates threshold (0.85).
7
+ const MATCHER_DEDUP_COSINE = 0.85;
8
+ const PREDICTION_DEDUP_COSINE = 0.85;
9
+
10
+ // Minimum matcher cosine to consider a prediction relevant. Matches the
11
+ // auto-fire threshold in index.ts (PREDICTION_THRESHOLD). The query tool
12
+ // should not return predictions from near-zero-similarity matchers that
13
+ // would never fire in the auto-fire.
14
+ const PREDICTION_QUERY_THRESHOLD = 0.45;
15
+
5
16
  /**
6
17
  * Shared context passed to every tool's execute function. Framework-agnostic —
7
18
  * neither opencode nor MCP specific. The plugin wires real defaults; tests and
@@ -367,6 +378,194 @@ const extractionDoneDef: ToolDef = {
367
378
  },
368
379
  };
369
380
 
381
+ // ---------------------------------------------------------------------------
382
+ // Prediction engine: user decision model
383
+ // ---------------------------------------------------------------------------
384
+
385
+ const predictionQueryDef: ToolDef = {
386
+ name: "prediction_query",
387
+ description:
388
+ "Query the user decision model for scored predictions matching a " +
389
+ "context. Returns predictions with confidence and evidence count. " +
390
+ "Use when facing a judgment call about scope, appropriateness, or " +
391
+ "methodology and the auto-injected prediction block did not already " +
392
+ "cover the situation.",
393
+ args: {
394
+ context: z.string().describe(
395
+ "The situation or context to match against. Describe the decision being faced.",
396
+ ),
397
+ store: z.string().optional().describe(
398
+ "Store to search. Defaults to the project store and global together.",
399
+ ),
400
+ },
401
+ async execute(args, ctx) {
402
+ const stores = args.store
403
+ ? [args.store as string]
404
+ : [ctx.defaultStore, "global"];
405
+ const embedding = await ctx.model.queryEmbed(args.context as string);
406
+ const matchers = ctx.db.findMatchers(stores, embedding, { limit: 5 })
407
+ .filter((m) => m.score >= PREDICTION_QUERY_THRESHOLD);
408
+ if (matchers.length === 0) return "No matching predictions found.";
409
+ const scored = ctx.db.scorePredictions(matchers);
410
+ if (scored.length === 0) return "No matching predictions found.";
411
+ return scored.map((s) => {
412
+ const verb = s.evidence_count === 0 ? "you may prefer" : "you tend to";
413
+ return `[${s.confidence.toFixed(2)} conf, ${s.evidence_count} tests] ` +
414
+ `When ${s.matcher_description}: ${verb} ${s.statement}`;
415
+ }).join("\n");
416
+ },
417
+ };
418
+
419
+ const predictionUpdateDef: ToolDef = {
420
+ name: "prediction_update",
421
+ description:
422
+ "Create, reinforce, or weaken a prediction in the user decision " +
423
+ "model. Use when the user corrects you, answers a question, or " +
424
+ "provides a clear signal about their preferences or decision-making " +
425
+ "strategy. The tool handles matcher and prediction lookup, dedup, " +
426
+ "and confidence adjustment automatically.",
427
+ args: {
428
+ matcher: z.string().describe(
429
+ "Description of the situation. What decision was being made?",
430
+ ),
431
+ prediction: z.string().describe(
432
+ "The user's preference or tendency in this situation.",
433
+ ),
434
+ signal: z.enum(["confirm", "disconfirm", "soft", "create"]).describe(
435
+ "What happened: confirm (user confirmed the prediction), " +
436
+ "disconfirm (user pushed back), soft (weak disconfirm, user " +
437
+ "partially disagreed), create (new observation).",
438
+ ),
439
+ rationale: z.string().describe(
440
+ "Why this prediction was formed or updated. What did the user say or do?",
441
+ ),
442
+ store: z.string().optional().describe(
443
+ "Store to write to. Defaults to the project store.",
444
+ ),
445
+ },
446
+ async execute(args, ctx) {
447
+ const store = (args.store as string) || ctx.defaultStore;
448
+ const matcherText = args.matcher as string;
449
+ const predictionText = args.prediction as string;
450
+ const signal = args.signal as "confirm" | "disconfirm" | "soft" | "create";
451
+ const rationale = args.rationale as string;
452
+
453
+ const matcherEmbed = await ctx.model.passageEmbed(matcherText);
454
+ const predEmbed = await ctx.model.passageEmbed(predictionText);
455
+
456
+ // All read-modify-write mutations run inside a transaction so a
457
+ // failure mid-sequence (FK violation, I/O error) rolls back the
458
+ // entire operation rather than leaving orphans (matcher without
459
+ // prediction, prediction without edge, edge without confidence).
460
+ return ctx.db.transaction(() => {
461
+ // Dedup matchers: find an existing matcher above the cosine threshold
462
+ // rather than always creating a new one.
463
+ let matcherId = ctx.db.findNearestMatcher(store, matcherEmbed, MATCHER_DEDUP_COSINE)?.id;
464
+ if (!matcherId) matcherId = ctx.db.createMatcher(store, matcherText, matcherEmbed, ctx.model.name);
465
+
466
+ // Store-wide dedup: search the entire store for a near-identical
467
+ // prediction, not just this matcher's edges. If found, link this
468
+ // matcher to the existing prediction via an edge rather than
469
+ // creating a second row with the same statement.
470
+ let predictionId = ctx.db.findNearestPrediction(store, predEmbed, PREDICTION_DEDUP_COSINE)?.id;
471
+
472
+ if (!predictionId) {
473
+ // No near-identical prediction exists; create one and link it.
474
+ predictionId = ctx.db.createPrediction(store, predictionText, rationale, predEmbed, ctx.model.name);
475
+ ctx.db.createEdge(matcherId, predictionId, 1.0);
476
+
477
+ // When the signal is confirm/disconfirm/soft (not create), apply
478
+ // it immediately so the first signal isn't lost. The prediction
479
+ // starts at p0 with 0 evidence; without this, the agent's
480
+ // "confirm" would be discarded and the prediction would have
481
+ // 0 confirms.
482
+ if (signal !== "create") {
483
+ ctx.db.adjustConfidence(predictionId, signal === "soft" ? "soft" : signal === "confirm" ? "confirm" : "disconfirm");
484
+ ctx.db.addProvenance(predictionId, signal, rationale);
485
+ const updated = ctx.db.getPrediction(predictionId);
486
+ return `[created + ${signal}] "${updated?.statement ?? predictionText}" confidence=${(updated?.confidence ?? 0).toFixed(2)} (${updated?.confirm_count ?? 0}/${updated?.disconfirm_count ?? 0})`;
487
+ }
488
+ ctx.db.addProvenance(predictionId, "create", rationale);
489
+ return `[created] ${store} :: "${predictionText}" for "${matcherText}"`;
490
+ }
491
+
492
+ // Ensure an edge links this matcher to the existing prediction.
493
+ // createEdge uses ON CONFLICT DO NOTHING, so existing edge weights
494
+ // are preserved.
495
+ ctx.db.createEdge(matcherId, predictionId, 1.0);
496
+
497
+ // "create" on an existing prediction means the agent re-observed
498
+ // the same preference in a new context. Link the edge and record
499
+ // provenance, but do NOT adjust confidence — "create" is
500
+ // confidence-neutral, not a disconfirm.
501
+ if (signal === "create") {
502
+ ctx.db.addProvenance(predictionId, "create", rationale);
503
+ const existing = ctx.db.getPrediction(predictionId);
504
+ return `[linked] "${existing?.statement ?? predictionText}" for "${matcherText}" confidence=${(existing?.confidence ?? 0).toFixed(2)} (${existing?.confirm_count ?? 0}/${existing?.disconfirm_count ?? 0})`;
505
+ }
506
+
507
+ // Signal is confirm, disconfirm, or soft. Map the tool's 4-value
508
+ // enum to adjustConfidence's 3-value enum.
509
+ ctx.db.adjustConfidence(predictionId, signal === "soft" ? "soft" : signal === "confirm" ? "confirm" : "disconfirm");
510
+ ctx.db.addProvenance(predictionId, signal, rationale);
511
+ const updated = ctx.db.getPrediction(predictionId);
512
+ return `[${signal}] "${updated?.statement ?? predictionText}" confidence=${(updated?.confidence ?? 0).toFixed(2)} (${updated?.confirm_count ?? 0}/${updated?.disconfirm_count ?? 0})`;
513
+ });
514
+ },
515
+ };
516
+
517
+ const predictionListDef: ToolDef = {
518
+ name: "prediction_list",
519
+ description:
520
+ "List all predictions in the user decision model with their matchers, " +
521
+ "confidence, and evidence count. For inspection and debugging.",
522
+ args: {
523
+ store: z.string().optional().describe(
524
+ "Which store to list. Defaults to the project store.",
525
+ ),
526
+ },
527
+ async execute(args, ctx) {
528
+ const store = (args.store as string) || ctx.defaultStore;
529
+ const predictions = ctx.db.listPredictions(store);
530
+ if (predictions.length === 0) return `No predictions in "${store}".`;
531
+ return predictions.map((p) => {
532
+ const matchers = p.matchers.map((m) => ` - "${m.description}" (w:${m.weight})`).join("\n");
533
+ const provenance = ctx.db.getProvenance(p.id);
534
+ const provLines = provenance.map((pr) => ` - [${pr.created_at.slice(0, 10)}] ${pr.signal}: ${pr.detail ?? ""}`).join("\n");
535
+ return `[${p.confidence.toFixed(2)} conf, ${p.evidence_count} tests] ${p.statement}` +
536
+ (p.rationale ? `\n rationale: ${p.rationale}` : "") +
537
+ (matchers ? `\n matchers:\n${matchers}` : "") +
538
+ (provLines ? `\n provenance:\n${provLines}` : "");
539
+ }).join("\n\n");
540
+ },
541
+ };
542
+
543
+ const predictionDeleteDef: ToolDef = {
544
+ name: "prediction_delete",
545
+ description:
546
+ "Delete a prediction from the user decision model. Useful when a " +
547
+ "prediction was created in error or is no longer relevant. Edges " +
548
+ "and provenance are deleted automatically (cascade).",
549
+ args: {
550
+ statement: z.string().describe(
551
+ "The prediction statement to delete. Use prediction_list to find the exact text; matching is semantic (cosine >= 0.85).",
552
+ ),
553
+ store: z.string().optional().describe(
554
+ "Store to delete from. Defaults to the project store.",
555
+ ),
556
+ },
557
+ async execute(args, ctx) {
558
+ const store = (args.store as string) || ctx.defaultStore;
559
+ const statementText = args.statement as string;
560
+ const predEmbed = await ctx.model.passageEmbed(statementText);
561
+ const prediction = ctx.db.findNearestPrediction(store, predEmbed, PREDICTION_DEDUP_COSINE);
562
+ if (!prediction) return `No prediction matching "${statementText}" found in "${store}".`;
563
+ const deleted = ctx.db.deletePrediction(prediction.id);
564
+ if (!deleted) return `Failed to delete prediction "${prediction.statement}".`;
565
+ return `[deleted] "${prediction.statement}" from "${store}"`;
566
+ },
567
+ };
568
+
370
569
  /**
371
570
  * All tool definitions, in the order they should be presented to the agent.
372
571
  * The opencode plugin wraps each in `tool()`; the MCP server exposes them
@@ -382,4 +581,8 @@ export const TOOL_DEFS: ToolDef[] = [
382
581
  findDuplicatesDef,
383
582
  markCheckedDef,
384
583
  extractionDoneDef,
584
+ predictionQueryDef,
585
+ predictionUpdateDef,
586
+ predictionListDef,
587
+ predictionDeleteDef,
385
588
  ];