@jeffober/thatch 0.1.39 → 0.1.41
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/README.md +10 -3
- package/artifacts/skills/common.md +4 -0
- package/artifacts/skills/thatch-code-review.md +10 -4
- package/artifacts/skills/thatch-review-synthesizer.md +57 -6
- package/artifacts/skills/thatch-review-technique.md +173 -0
- package/bin/thatch +160 -16
- package/docs/dev/README.md +26 -14
- package/docs/dev/features/README.md +1 -0
- package/docs/dev/features/behavior-engine.md +5 -3
- package/docs/dev/features/cli.md +22 -4
- package/docs/dev/features/cross-session-chat.md +256 -0
- package/docs/dev/features/database.md +52 -4
- package/docs/dev/features/memory-store.md +13 -0
- package/docs/dev/features/multi-host.md +17 -9
- package/docs/dev/features/prediction-engine.md +6 -3
- package/docs/dev/features/setup.md +12 -5
- package/docs/dev/gotchas.md +7 -4
- package/docs/dev/mcp-parity.md +2 -2
- package/docs/dev/setup-and-hooks.md +5 -4
- package/docs/dev/skills.md +7 -3
- package/docs/user/README.md +24 -4
- package/docs/user/cli.md +30 -0
- package/docs/user/code-review.md +4 -3
- package/docs/user/cross-session-chat.md +102 -0
- package/docs/user/memory.md +3 -0
- package/docs/user/setup.md +20 -4
- package/docs/user/skills.md +7 -5
- package/package.json +2 -1
- package/src/behavior.ts +1 -1
- package/src/chat-names.ts +121 -0
- package/src/chat.ts +879 -0
- package/src/db.ts +181 -2
- package/src/embeddings.ts +152 -9
- package/src/index.ts +113 -5
- package/src/prediction.ts +1 -1
- package/src/prompts.ts +133 -8
- package/src/scoring-engine.ts +25 -4
- package/src/setup.ts +83 -29
- package/src/skills.ts +47 -16
- package/src/tool-defs.ts +422 -40
package/bin/thatch
CHANGED
|
@@ -10,6 +10,7 @@ import { hygieneReport } from "../src/hygiene";
|
|
|
10
10
|
import { claudeSessionStartReminder } from "../src/prompts";
|
|
11
11
|
import { setupClaudeCode, setupCursor } from "../src/setup";
|
|
12
12
|
import { resolveOpencodeDbPath, SessionDB, partToTimelineEntry, partToTranscriptEntries, partToFullJson, messageToFullJson, type SessionPart } from "../src/session-db";
|
|
13
|
+
import { chatTailDiff, formatChatTailEvent, isStale, CHAT_STALE_MINUTES } from "../src/chat";
|
|
13
14
|
|
|
14
15
|
const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
15
16
|
const dbPath = process.env.THATCH_DB_PATH ?? join(configHome, "thatch", "thatch.db");
|
|
@@ -33,7 +34,7 @@ function usage(): never {
|
|
|
33
34
|
thatch buffer-tool Append a single postToolUse interaction to the queue (Cursor hook only)
|
|
34
35
|
thatch flush-tools [--json] Peek the queue + print an extraction nudge (hook only)
|
|
35
36
|
thatch flush-predictions [--json] Query predictions for the prompt (hook only)
|
|
36
|
-
thatch setup --claude [--cursor] [--global]
|
|
37
|
+
thatch setup --claude [--cursor] [--local|--global]
|
|
37
38
|
Install config + instructions + hooks + skills
|
|
38
39
|
thatch session list -s <id> [--after T] [--before T]
|
|
39
40
|
JSONL timeline of a session's parts (one line per part)
|
|
@@ -44,6 +45,9 @@ function usage(): never {
|
|
|
44
45
|
Full conversation as OpenAI chat-completions JSONL
|
|
45
46
|
thatch session search <query> [--session <id>] [--regex] [--limit N]
|
|
46
47
|
JSONL of matching parts across sessions (decoded content)
|
|
48
|
+
thatch chat list Registered chat sessions: name, age, project, topic
|
|
49
|
+
thatch chat tail [--once] Follow cross-session chat: sent and read events
|
|
50
|
+
(--once prints current state and exits)
|
|
47
51
|
|
|
48
52
|
Stores default to the repo detected from git remote. Use "global" for the
|
|
49
53
|
global store. Use "all" with search to search project + global together.`);
|
|
@@ -58,13 +62,39 @@ async function defaultStore(): Promise<string> {
|
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Builds the chat-mail hook line for the given project: one line when
|
|
67
|
+
* unread mail is waiting for registered sessions, naming the addressee
|
|
68
|
+
* sessions. Empty string when the project has no chat state or no pending
|
|
69
|
+
* mail - a session that never registered never sees a chat line at all,
|
|
70
|
+
* and the line is about the addressee, not about the reader, so there is
|
|
71
|
+
* nothing for an unregistered model to hallucinate.
|
|
72
|
+
*/
|
|
73
|
+
async function chatHookLine(): Promise<string> {
|
|
74
|
+
let repo: string | null = null;
|
|
75
|
+
try {
|
|
76
|
+
repo = await detectRepo();
|
|
77
|
+
} catch {
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
const pending = db.chatPendingByProject(repo);
|
|
81
|
+
if (pending.length === 0) return "";
|
|
82
|
+
const parts = pending.map((p) => `${p.name} (${p.pending})`);
|
|
83
|
+
return `[thatch] chat: unread message(s) for ${parts.join(", ")} - the addressee should call chat_read`;
|
|
84
|
+
}
|
|
85
|
+
|
|
61
86
|
/**
|
|
62
87
|
* Emits the nudge output, prepending a version warning if present. The
|
|
63
88
|
* warning goes first so the LLM sees it before the nudge text. Used by
|
|
64
89
|
* flush-tools to surface version skew and update-available notices on
|
|
65
|
-
* every UserPromptSubmit, regardless of which tier fired.
|
|
90
|
+
* every UserPromptSubmit, regardless of which tier fired. The chat hook
|
|
91
|
+
* line, when present, is appended last (the mail check is the last thing
|
|
92
|
+
* the model reads before the nudge body).
|
|
66
93
|
*/
|
|
67
|
-
function emit(output: string, versionWarning: string | null, isJson: boolean): void {
|
|
94
|
+
function emit(output: string, versionWarning: string | null, isJson: boolean, chatLine = ""): void {
|
|
95
|
+
if (chatLine) {
|
|
96
|
+
output = `${output}\n\n${chatLine}`;
|
|
97
|
+
}
|
|
68
98
|
if (versionWarning) {
|
|
69
99
|
output = `[thatch] ${versionWarning}\n\n${output}`;
|
|
70
100
|
}
|
|
@@ -219,6 +249,92 @@ async function sessionCommand(rest: string[]): Promise<void> {
|
|
|
219
249
|
}
|
|
220
250
|
}
|
|
221
251
|
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
// chat subcommand group - read-only window on the cross-session chat
|
|
254
|
+
// ---------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
/** "2m ago", "3h ago", "5d ago" - human-readable age for the chat roster. */
|
|
257
|
+
function humanAge(iso: string, now = Date.now()): string {
|
|
258
|
+
const seconds = Math.max(0, Math.floor((now - Date.parse(iso)) / 1000));
|
|
259
|
+
if (Number.isNaN(seconds)) return "unknown age";
|
|
260
|
+
if (seconds < 60) return `${seconds}s ago`;
|
|
261
|
+
const minutes = Math.floor(seconds / 60);
|
|
262
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
263
|
+
if (minutes < 24 * 60) return `${Math.floor(minutes / 60)}h ago`;
|
|
264
|
+
return `${Math.floor(minutes / (24 * 60))}d ago`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function chatCommand(rest: string[]): Promise<void> {
|
|
268
|
+
const [sub, ...restArgs] = rest;
|
|
269
|
+
|
|
270
|
+
switch (sub) {
|
|
271
|
+
case "list": {
|
|
272
|
+
const sessions = db.listChatSessions();
|
|
273
|
+
if (sessions.length === 0) {
|
|
274
|
+
console.log("No sessions registered. Sessions join with thatch_chat_register from opencode.");
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
for (const s of sessions) {
|
|
278
|
+
// Same staleness convention as the chat_list tool: an age past the
|
|
279
|
+
// threshold means the session's host process is probably gone.
|
|
280
|
+
const liveness = isStale(s, CHAT_STALE_MINUTES) ? "STALE" : "fresh";
|
|
281
|
+
const parts = [
|
|
282
|
+
s.name,
|
|
283
|
+
`(${humanAge(s.last_seen)})`,
|
|
284
|
+
liveness,
|
|
285
|
+
`project:${s.project ?? "unknown"}`,
|
|
286
|
+
];
|
|
287
|
+
if (s.topic) parts.push(s.topic);
|
|
288
|
+
console.log(parts.join(" "));
|
|
289
|
+
}
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case "tail": {
|
|
294
|
+
const onceOnly = restArgs.includes("--once");
|
|
295
|
+
// Diff state: one entry per seen message id holding its read_at, so
|
|
296
|
+
// each poll emits only new messages and newly-read rows. The diff
|
|
297
|
+
// logic lives in src/chat.ts (chatTailDiff), unit-tested there.
|
|
298
|
+
const state = new Map<number, string | null>();
|
|
299
|
+
|
|
300
|
+
// Follow mode holds the handle open until Ctrl-C; the finally below
|
|
301
|
+
// closes it on every exit path, including the SIGINT handler's.
|
|
302
|
+
try {
|
|
303
|
+
// First pass: print the backlog (every existing row as its sent
|
|
304
|
+
// line), then follow unless --once.
|
|
305
|
+
for (const event of chatTailDiff(state, db.chatMessageFeed()).events) {
|
|
306
|
+
if (event.kind === "sent") console.log(formatChatTailEvent(event));
|
|
307
|
+
}
|
|
308
|
+
if (onceOnly) break;
|
|
309
|
+
|
|
310
|
+
process.on("SIGINT", () => process.exit(0));
|
|
311
|
+
for (;;) {
|
|
312
|
+
await Bun.sleep(2000);
|
|
313
|
+
try {
|
|
314
|
+
for (const event of chatTailDiff(state, db.chatMessageFeed()).events) {
|
|
315
|
+
console.log(formatChatTailEvent(event));
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
// The surface's whole job is watching a shared, mutable file;
|
|
319
|
+
// a feed failure should say so, not die with a stack trace.
|
|
320
|
+
console.error(`[thatch chat tail] feed query failed: ${err}`);
|
|
321
|
+
console.error("The database may be locked or missing; exiting.");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} finally {
|
|
326
|
+
db.close();
|
|
327
|
+
// The script tail also closes; closing twice is harmless and the
|
|
328
|
+
// early-return paths need this one.
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
default:
|
|
334
|
+
usage();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
222
338
|
switch (command) {
|
|
223
339
|
case "--version":
|
|
224
340
|
case "-v":
|
|
@@ -327,7 +443,10 @@ switch (command) {
|
|
|
327
443
|
} catch (err) {
|
|
328
444
|
console.error(`[thatch] hygiene report failed: ${err}`);
|
|
329
445
|
}
|
|
330
|
-
|
|
446
|
+
// Chat mail check: same gating as flush-tools - a line only when
|
|
447
|
+
// pending mail exists for registered sessions in this project.
|
|
448
|
+
const chatLine = await chatHookLine();
|
|
449
|
+
const reminder = claudeSessionStartReminder(repo, hygiene) + (chatLine ? `\n\n${chatLine}` : "");
|
|
331
450
|
if (isJson) {
|
|
332
451
|
console.log(JSON.stringify({ additional_context: reminder }));
|
|
333
452
|
} else {
|
|
@@ -383,12 +502,19 @@ switch (command) {
|
|
|
383
502
|
|
|
384
503
|
case "setup": {
|
|
385
504
|
const isGlobal = args.includes("--global");
|
|
505
|
+
// --local is the default scope; accepted explicitly so the flag the
|
|
506
|
+
// user types always matches what the command does.
|
|
507
|
+
const isLocal = args.includes("--local");
|
|
386
508
|
const isClaude = args.includes("--claude");
|
|
387
509
|
const isCursor = args.includes("--cursor");
|
|
388
510
|
|
|
511
|
+
if (isGlobal && isLocal) {
|
|
512
|
+
console.error("Choose one scope: --local or --global.");
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
389
515
|
if (!isClaude && !isCursor) {
|
|
390
516
|
console.error("Specify at least one host: --claude and/or --cursor.");
|
|
391
|
-
console.error("Usage: thatch setup --claude [--cursor] [--global]");
|
|
517
|
+
console.error("Usage: thatch setup --claude [--cursor] [--local|--global]");
|
|
392
518
|
process.exit(1);
|
|
393
519
|
}
|
|
394
520
|
|
|
@@ -400,6 +526,22 @@ switch (command) {
|
|
|
400
526
|
const thatchBin = thatchOnPath ?? resolve(process.argv[1] ?? process.execPath);
|
|
401
527
|
const dir = projectDir();
|
|
402
528
|
|
|
529
|
+
// Shared reporting: where skills went and what the pass did, so an
|
|
530
|
+
// idempotent re-run still tells the user something true.
|
|
531
|
+
const printSkills = (result: { skills: { dir: string; added: unknown[]; updated: unknown[]; removed: string[]; unchanged: string[] }; otherScopeSkills: { dir: string; count: number } | null; global: boolean }) => {
|
|
532
|
+
const { added, updated, removed, unchanged } = result.skills;
|
|
533
|
+
console.log(` Skills: ${result.skills.dir}`);
|
|
534
|
+
console.log(` ${added.length} added, ${updated.length} updated, ${removed.length} removed, ${unchanged.length} unchanged`);
|
|
535
|
+
if (result.otherScopeSkills) {
|
|
536
|
+
const { dir: otherDir, count } = result.otherScopeSkills;
|
|
537
|
+
if (result.global) {
|
|
538
|
+
console.log(` Note: ${count} thatch skills exist in ${otherDir} (project scope); --global does not update them.`);
|
|
539
|
+
} else {
|
|
540
|
+
console.log(` Note: ${count} thatch skills remain in ${otherDir} (user scope; not updated by this run). Run setup with --global to refresh them, or remove them if this repo now owns its skills.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
403
545
|
if (isClaude) {
|
|
404
546
|
const result = setupClaudeCode(thatchBin, isGlobal, dir);
|
|
405
547
|
|
|
@@ -410,10 +552,7 @@ switch (command) {
|
|
|
410
552
|
}
|
|
411
553
|
console.log(` Instructions: ${result.claudeMd}`);
|
|
412
554
|
console.log(` Hooks: ${result.settings}`);
|
|
413
|
-
|
|
414
|
-
? result.skills[0].path.split("/").slice(0, -1).join("/")
|
|
415
|
-
: "(no skills installed)";
|
|
416
|
-
console.log(` Skills: ${skillsDir} (${result.skills.length} files)`);
|
|
555
|
+
printSkills(result);
|
|
417
556
|
console.log();
|
|
418
557
|
if (result.mcpAddCommand) {
|
|
419
558
|
console.log("To register the MCP server globally, run:");
|
|
@@ -432,10 +571,7 @@ switch (command) {
|
|
|
432
571
|
console.log(` MCP config: ${result.mcpConfig}`);
|
|
433
572
|
console.log(` Instructions: ${result.agentsMd}`);
|
|
434
573
|
console.log(` Hooks: ${result.hooks}`);
|
|
435
|
-
|
|
436
|
-
? result.skills[0].path.split("/").slice(0, -1).join("/")
|
|
437
|
-
: "(no skills installed)";
|
|
438
|
-
console.log(` Skills: ${skillsDir} (${result.skills.length} files)`);
|
|
574
|
+
printSkills(result);
|
|
439
575
|
console.log();
|
|
440
576
|
console.log("Restart Cursor for the changes to take effect.");
|
|
441
577
|
}
|
|
@@ -447,6 +583,11 @@ switch (command) {
|
|
|
447
583
|
break;
|
|
448
584
|
}
|
|
449
585
|
|
|
586
|
+
case "chat": {
|
|
587
|
+
await chatCommand(args);
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
|
|
450
591
|
case "buffer-batch": {
|
|
451
592
|
// PostToolBatch hook: append the batch's tool_calls to the session's
|
|
452
593
|
// JSONL queue. Silent on success - no stdout - so the next model call
|
|
@@ -545,6 +686,9 @@ switch (command) {
|
|
|
545
686
|
// flush-tools fires on every prompt, including conversational turns
|
|
546
687
|
// with no tool calls.
|
|
547
688
|
const versionWarning = detectSkew(dbPath) ?? readNpmCacheForUpdate(dbPath);
|
|
689
|
+
// Chat mail check: one line when pending mail exists for registered
|
|
690
|
+
// sessions in this project, empty string otherwise (see chatHookLine).
|
|
691
|
+
const chatLine = await chatHookLine();
|
|
548
692
|
|
|
549
693
|
// Tier 1: extraction nudge. Peek (don't drain) - the queue persists
|
|
550
694
|
// until the agent calls memory_remember, which triggers consumeQueue.
|
|
@@ -556,7 +700,7 @@ switch (command) {
|
|
|
556
700
|
const missed = sessionID ? getMissedCount(sessionID) : 0;
|
|
557
701
|
if (sessionID) incrementMissedCount(sessionID);
|
|
558
702
|
const output = extractionNudge(interactions.length, missed, "mcp__thatch__memory_remember", sessionID ?? "");
|
|
559
|
-
emit(output, versionWarning, isJson);
|
|
703
|
+
emit(output, versionWarning, isJson, chatLine);
|
|
560
704
|
break;
|
|
561
705
|
}
|
|
562
706
|
|
|
@@ -590,14 +734,14 @@ switch (command) {
|
|
|
590
734
|
|
|
591
735
|
if (parts.length > 0) {
|
|
592
736
|
const output = parts.join("\n\n");
|
|
593
|
-
emit(output, versionWarning, isJson);
|
|
737
|
+
emit(output, versionWarning, isJson, chatLine);
|
|
594
738
|
break;
|
|
595
739
|
}
|
|
596
740
|
}
|
|
597
741
|
|
|
598
742
|
// Tier 3: static write nudge.
|
|
599
743
|
const output = claudeWriteNudge();
|
|
600
|
-
emit(output, versionWarning, isJson);
|
|
744
|
+
emit(output, versionWarning, isJson, chatLine);
|
|
601
745
|
} catch (err) {
|
|
602
746
|
console.error(`[thatch flush-tools] failed: ${err}`);
|
|
603
747
|
try {
|
package/docs/dev/README.md
CHANGED
|
@@ -40,8 +40,11 @@ OpenCode plugin path
|
|
|
40
40
|
├── index.ts → plugin entry: wires DB/model/extraction, registers tools + hooks
|
|
41
41
|
├── tools.ts → thin opencode tool() wrappers over tool-defs
|
|
42
42
|
├── extraction.ts → in-memory ring buffer + shared payload builders
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
├── watchers.ts → in-memory watcher registry + poller (opencode-only,
|
|
44
|
+
│ event-driven PR notifications delivered as prompts)
|
|
45
|
+
└── chat.ts → cross-session chat: shared SQLite directory + inbox
|
|
46
|
+
(ChatStore) and the per-process wake-up poller
|
|
47
|
+
(ChatPoller, opencode-only)
|
|
45
48
|
|
|
46
49
|
MCP server path
|
|
47
50
|
├── mcp.ts → stdio JSON-RPC server: z.toJSONSchema() for tools/list,
|
|
@@ -54,7 +57,8 @@ MCP server path
|
|
|
54
57
|
CLAUDE.md / AGENTS.md, settings/hooks JSON, installs skills
|
|
55
58
|
|
|
56
59
|
bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygiene|
|
|
57
|
-
prime|buffer-batch|buffer-tool|flush-tools|flush-predictions|setup
|
|
60
|
+
prime|buffer-batch|buffer-tool|flush-tools|flush-predictions|setup|
|
|
61
|
+
session (archaeology)|chat (list|tail)
|
|
58
62
|
```
|
|
59
63
|
|
|
60
64
|
## Module responsibilities
|
|
@@ -76,8 +80,10 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
76
80
|
| `notify.ts` | Out-of-band notification dispatch: darwin (`osascript` banner + `/usr/bin/say` voice), linux (`notify-send` + `spd-say`/`espeak`), other platforms report unsupported. Injectable spawner (`CoreContext.spawner`) so tests never fire real commands. Backs the `notify_user` tool. |
|
|
77
81
|
| `session-db.ts` | Read-only access to the opencode session database (opencode.db): session timeline (`session list`), full part/message retrieval (`session get`), OpenAI chat-completions transcript builder (`session transcript`), and decoded-content search (`session search`). Backs the `thatch session` CLI subcommands and the opencode-only `session_search`/`session_get` tools. Path resolution honors `OPENCODE_DB`, then the XDG default; the db opens read-only so a thatch bug cannot corrupt host history. |
|
|
78
82
|
| `watchers.ts` | In-memory watcher registry and background poller for event-driven notifications (opencode-only). Watches GitHub PRs and branches via `gh api` (injected `GhRunner`), diffs against last-seen state, queues pointer-only events per session, and delivers them through a plugin-supplied callback when the session is idle. Process-lifetime state by design - no SQLite. |
|
|
83
|
+
| `chat.ts` | Cross-session chat between opencode sessions on one machine (opencode-only). `ChatStore` holds the shared directory and inbox in SQLite so any process can read/write them; `ChatPoller` heartbeats hosted sessions and delivers wake prompts through an injected callback when the recipient is idle - only the recipient's host process delivers, so there is no cross-process double-delivery. Heartbeat staleness flags crashed sessions; a per-recipient nudge rate cap is the anti-loop hard brake. See [features/cross-session-chat.md](features/cross-session-chat.md). |
|
|
84
|
+
| `chat-names.ts` | The static display-name pool for the chat directory: whimsical geek-culture names in fnord's Nomenclater style, baked in so name assignment never costs a model call. `chat_register` without a name draws from it. |
|
|
79
85
|
| `sideband.ts` | Unix domain socket server + client. The MCP server (long-lived, warm model) runs `SidebandServer` so one-shot hook processes can ask it to embed a prompt and search for matches without loading the model themselves. Handles three methods: `match` (recall nudge), `predictions` (prediction auto-fire), and `behaviors` (behavior auto-fire). Socket path is a hash of the DB path — both processes compute it independently. |
|
|
80
|
-
| `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), prediction nudge (`predictionNudge`), behavior nudge (`behaviorNudge`), prediction verb selection (`predictionVerb`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
|
|
86
|
+
| `prompts.ts` | Text constants: opencode system prompt, compaction context, session-start reminder, prompt-aware recall nudge (`recallNudge` / `claudeRecallNudge`), prediction nudge (`predictionNudge`), behavior nudge (`behaviorNudge`), prediction verb selection (`predictionVerb`), watcher notification (`watcherNotificationNudge`), version warning (`versionWarningNudge`), chat wake notification (`chatNotificationNudge`), chat transcript echo text and echo recognizer (`chatEchoText` / `isChatEchoParts`), Claude Code CLAUDE.md instructions, Cursor AGENTS.md instructions, Claude Code hook text. |
|
|
81
87
|
| `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (fact-extractor, dedup-classifier, project-primer, the review specialists, review synthesizer, review context, code archaeology, review followup, review response, change walkthrough, code walkthrough, session reflection, coding-workflow, thatch-pr-description, thatch-ticket-description, thatch-split-overlarge-pr, memory-verify, knowledge-export — work on all three hosts) and `OPENCODE_ONLY_SKILLS` (code-review coordinator — requires sub-agent support, not installed for Claude Code or Cursor). `installSkills(dir, skills)` defaults to `SHARED_SKILLS`; the opencode plugin passes `[...SHARED_SKILLS, ...OPENCODE_ONLY_SKILLS]`. |
|
|
82
88
|
| `scoring-engine.ts` | Generic four-table scoring engine with Bayesian confidence. Shared base for prediction and behavior engines — each wraps it with table-specific names. |
|
|
83
89
|
| `prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names. |
|
|
@@ -94,10 +100,10 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
94
100
|
| `experimental.chat.system.transform` | Appends the thatch system prompt (store names, usage rules). |
|
|
95
101
|
| `experimental.session.compacting` | Marks the session as compacting and appends re-familiarization context so a compacted session still knows thatch exists. |
|
|
96
102
|
| `experimental.compaction.autocontinue` | Clears the compacting flag so `chat.message` nudges resume. Without this, nudges that instruct tool calls would fire during summary generation where tools are blocked. The `chat.message` hook also clears the flag if it fires for a session still in the compacting set but the incoming message has no compaction-type part — this handles compaction failure, where the session would otherwise be stuck with nudges off forever. |
|
|
97
|
-
| `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. For child sessions (`childToParent.has(sessionID)`), also tracks metrics: `remember` with `overwrite:false` → new++, `overwrite:true` → updated++, `forget` → deleted++. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
|
|
98
|
-
| `chat.message` | Two priority tiers: (a) if extraction buffer has interactions and the session is NOT in the `extracting` set (direct extraction in progress), **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge with the session ID and fetch tool name (not the full payload) — the sub-agent calls `thatch_get_extraction_payload` to retrieve the interactions as a tool response, keeping the full payload out of the main session's context window. The buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire (`db.scorePredictionNudge`, injects `[thatch] User decision model`) and the behavior auto-fire (`db.scoreBehaviorNudge`, injects `[thatch] Situational behaviors`). All three nudges (recall, prediction, behavior) fire independently in separate try/catch blocks with separate synthetic parts. Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
|
|
99
|
-
| `event` | Subscribes to all session bus events. `session.created`: records `childToParent` + `parentSnapshots` (shallow copy of the parent's buffer for snapshot-aware drain), then sends the session-start reminder via `client.session.prompt` carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. `session.status`: records the session's live status for the watcher delivery
|
|
100
|
-
| `dispose` | Stops the version checker and the
|
|
103
|
+
| `tool.execute.after` | Buffers every non-`thatch_*`, non-`skill`, non-`task` tool call into the session's extraction buffer. (Skill/task are excluded — buffering them creates a feedback loop where the nudge triggers a skill load, which gets buffered, which triggers another nudge.) Memory writes (`thatch_memory_remember`) and `thatch_extraction_done` drain the buffer and reset the missed-nudge counter. For child sessions (`childToParent.has(sessionID)`), also tracks metrics: `remember` with `overwrite:false` → new++, `overwrite:true` → updated++, `forget` → deleted++. For `thatch_chat_*` tool calls, builds the transcript echo (`chatEchoText`) and posts it as a visible non-synthetic `noReply` part. This is a plugin hook, NOT a bus event — do not move it into the `event` handler; the event bus has no such event and it will silently never fire. |
|
|
104
|
+
| `chat.message` | Two priority tiers: (a) if extraction buffer has interactions and the session is NOT in the `extracting` set (direct extraction in progress), **peeks** the buffer (does NOT drain it) and injects a synthetic text part carrying the extraction nudge with the session ID and fetch tool name (not the full payload) — the sub-agent calls `thatch_get_extraction_payload` to retrieve the interactions as a tool response, keeping the full payload out of the main session's context window. The buffer persists until the agent writes a memory or calls `thatch_extraction_done`, so ignored nudges repeat and escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter; (b) otherwise, embeds the user's prompt text with the in-process warm model, searches `db.search()` across repo + global, and pushes a recall nudge if matches exceed the threshold (default 0.55). The same embedding also feeds the prediction auto-fire (`db.scorePredictionNudge`, injects `[thatch] User decision model`) and the behavior auto-fire (`db.scoreBehaviorNudge`, injects `[thatch] Situational behaviors`). All three nudges (recall, prediction, behavior) fire independently in separate try/catch blocks with separate synthetic parts. Chat transcript echoes (non-synthetic `noReply` parts whose text is entirely `[chat]`-prefixed bubbles) are skipped before any tier — no model turn reads them, so nudging them wastes an embedding. Skipped entirely while the session is compacting (tool calls are blocked during summary generation). |
|
|
105
|
+
| `event` | Subscribes to all session bus events. `session.created`: records `childToParent` + `parentSnapshots` (shallow copy of the parent's buffer for snapshot-aware drain), then sends the session-start reminder via `client.session.prompt` carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. `session.status`: records the session's live status for the watcher and chat delivery gates; on idle, if the session is a child, drains its snapshot, fires a toast with `childMetrics`, and deletes the child session; if the session is a parent with pending tool interactions and not already extracting, calls `triggerExtraction` to create a direct-extraction child, then flushes pending watcher events and pending chat mail (the session just became deliverable). `session.error`: requeues the parent's buffer (child died without draining). `session.deleted`: cleans up `childToParent`, `parentSnapshots`, `childMetrics`, and `extracting`, cancels the session's watchers, and unregisters it from the chat directory (the crash path — a dead process fires no event — is covered by heartbeat staleness). `session.compacted`: clears the compacting flag so `chat.message` nudges resume. |
|
|
106
|
+
| `dispose` | Stops the version checker, the watcher poller, and the chat poller, closes the DB. |
|
|
101
107
|
|
|
102
108
|
Hook failures are logged with a `[thatch]` prefix — never swallowed silently.
|
|
103
109
|
Two of these hooks were dead for weeks because failures were invisible.
|
|
@@ -119,10 +125,12 @@ Two of these hooks were dead for weeks because failures were invisible.
|
|
|
119
125
|
5. **Store creation is implicit.** First `remember` to a new store creates it.
|
|
120
126
|
6. **Default recall scope is repo + global.** The tool layer hardcodes this.
|
|
121
127
|
7. **Skills are plugin-owned files.** Installed to
|
|
122
|
-
`$XDG_CONFIG_HOME/opencode/skills` (opencode),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
`$XDG_CONFIG_HOME/opencode/skills` (opencode); for MCP hosts,
|
|
129
|
+
scope-dependent — the repo's `.claude/skills/` or `.cursor/skills/`
|
|
130
|
+
for project-local `thatch setup`, `~/.claude/skills/` or
|
|
131
|
+
`~/.cursor/skills/` for `--global`. Drifted content is overwritten
|
|
132
|
+
on plugin init or re-running `thatch setup`. Skills are split into
|
|
133
|
+
`SHARED_SKILLS` (work on all
|
|
126
134
|
three hosts) and `OPENCODE_ONLY_SKILLS` (require sub-agent support,
|
|
127
135
|
not installed for Claude Code or Cursor). The opencode plugin
|
|
128
136
|
installs both arrays; `thatch setup --claude` and `--cursor` install
|
|
@@ -328,12 +336,16 @@ behavior cycle (agent-driven, self-graded)
|
|
|
328
336
|
`behavior_matchers(id PK, store, description, embedding BLOB, model, created_at, updated_at)`,
|
|
329
337
|
`behaviors(id PK, store, statement, rationale, embedding BLOB, model, confidence REAL, confirm_count REAL, disconfirm_count REAL, created_at, updated_at)`,
|
|
330
338
|
`behavior_edges(matcher_id, behavior_id, weight REAL, PK(matcher_id, behavior_id), FK CASCADE)`,
|
|
331
|
-
`behavior_provenance(id PK, behavior_id, signal, detail, created_at, FK CASCADE)
|
|
339
|
+
`behavior_provenance(id PK, behavior_id, signal, detail, created_at, FK CASCADE)`,
|
|
340
|
+
`chat_sessions(session_id PK, name UNIQUE COLLATE NOCASE, project, registered_at, last_seen)`,
|
|
341
|
+
`chat_messages(id PK AUTOINCREMENT, from_session, to_session, body, created_at, delivered_at, read_at)`.
|
|
332
342
|
- `recall_count`, `last_recalled_at`, and `archived` are added to pre-existing
|
|
333
343
|
databases by an idempotent column migration at init (`PRAGMA table_info` +
|
|
334
344
|
`ALTER TABLE`). The `archived` column is `INTEGER NOT NULL DEFAULT 0` (0 =
|
|
335
345
|
live, 1 = archived); search, dedup, and staleness queries all exclude
|
|
336
|
-
archived entries by default.
|
|
346
|
+
archived entries by default. A second init migration rebuilds a
|
|
347
|
+
pre-NOCASE `chat_sessions` table to enforce case-insensitive name
|
|
348
|
+
uniqueness (see [features/database.md](features/database.md)).
|
|
337
349
|
- Embeddings are raw Float32Array bytes. Serialization honors
|
|
338
350
|
`byteOffset`/`byteLength` — transformers.js can return views into larger
|
|
339
351
|
tensor buffers, and serializing the whole backing buffer corrupts vectors.
|
|
@@ -27,6 +27,7 @@ system, see [../skills.md](../skills.md). For non-obvious invariants, see
|
|
|
27
27
|
| [compaction-recovery.md](compaction-recovery.md) | Compaction recovery (opencode compaction hooks) |
|
|
28
28
|
| [session-lifecycle.md](session-lifecycle.md) | Session lifecycle management (opencode events, child tracking) |
|
|
29
29
|
| [watchers.md](watchers.md) | Event-driven notifications (in-memory registry, background poller, PR watching) |
|
|
30
|
+
| [cross-session-chat.md](cross-session-chat.md) | Cross-session chat (shared SQLite directory and inbox, per-process wake-up delivery) |
|
|
30
31
|
| [notifications.md](notifications.md) | Out-of-band notification (banner + voice) and the user config file |
|
|
31
32
|
| [setup.md](setup.md) | Setup system (markers, checkSetup, auto-refresh, binary resolution) |
|
|
32
33
|
| [cli.md](cli.md) | CLI (subcommands, environment variables) |
|
|
@@ -78,9 +78,11 @@ Runs in a single DB transaction:
|
|
|
78
78
|
|
|
79
79
|
1. **Matcher dedup** — finds an existing behavior matcher above cosine 0.85,
|
|
80
80
|
else creates one.
|
|
81
|
-
2. **Behavior dedup** — store
|
|
82
|
-
(cosine >= 0.85). If found, links this matcher to
|
|
83
|
-
than creating a duplicate row.
|
|
81
|
+
2. **Behavior dedup** — search across the target store AND global for a
|
|
82
|
+
near-identical behavior (cosine >= 0.85). If found, links this matcher to
|
|
83
|
+
it via an edge rather than creating a duplicate row. The auto-fire nudge
|
|
84
|
+
scans both stores, so the write reinforces the existing row in its home
|
|
85
|
+
store and the result echoes where it landed.
|
|
84
86
|
3. **New behavior** — creates behavior + edge + provenance entry tagged
|
|
85
87
|
`codify`.
|
|
86
88
|
4. Returns `[codified]` for a new behavior or `[linked]` for an existing one
|
package/docs/dev/features/cli.md
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# CLI (bin/thatch)
|
|
2
2
|
|
|
3
|
-
The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, MCP server startup, hook commands for MCP hosts, and the
|
|
3
|
+
The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, MCP server startup, hook commands for MCP hosts, the setup installer, session archaeology over the opencode database, and a read-only window on the cross-session chat directory.
|
|
4
4
|
|
|
5
5
|
## Subcommands
|
|
6
6
|
|
|
7
|
-
15 subcommands:
|
|
8
|
-
|
|
9
7
|
| Command | Args | Flags | Stdin | Purpose |
|
|
10
8
|
|---------|------|-------|-------|---------|
|
|
11
9
|
| `stores` | none | none | none | List all store names |
|
|
@@ -22,6 +20,9 @@ The thatch CLI is a Bun script at `bin/thatch`. It provides memory inspection, M
|
|
|
22
20
|
| `flush-tools [--json]` | none | `--json` | JSON | Peek queue + extraction/recall/prediction/behavior/write nudge |
|
|
23
21
|
| `flush-predictions [--json]` | none | `--json` | JSON | Standalone prediction-only nudge |
|
|
24
22
|
| `setup --claude [--cursor] [--global]` | none | `--claude`, `--cursor`, `--global` | none | Install config + instructions + hooks + skills |
|
|
23
|
+
| `session list/get/transcript/search` | per subcommand | `-s/--session`, `--id`, `--after`, `--before`, `--regex`, `--limit` | none | Read-only archaeology on the opencode session database (JSONL output) |
|
|
24
|
+
| `chat list` | none | none | none | Registered chat sessions: name, human-readable age, project, topic |
|
|
25
|
+
| `chat tail [--once]` | none | `--once` | none | Follow cross-session chat: sent and read events (`--once` prints the backlog and exits) |
|
|
25
26
|
| (unknown) | none | none | none | Print usage, exit 1 |
|
|
26
27
|
|
|
27
28
|
## Global behavior
|
|
@@ -41,6 +42,21 @@ Runs the thatch-project-primer skill via an external CLI. Searches `PATH` in ord
|
|
|
41
42
|
|
|
42
43
|
Inherits stdio, exits with the child's exit code. Errors and exits 1 if none found.
|
|
43
44
|
|
|
45
|
+
## chat
|
|
46
|
+
|
|
47
|
+
Read-only window on the cross-session chat directory ([cross-session-chat.md](cross-session-chat.md)):
|
|
48
|
+
|
|
49
|
+
- `chat list` renders the roster: display name, human-readable age since the
|
|
50
|
+
last heartbeat, project, and topic when set.
|
|
51
|
+
- `chat tail [--once]` follows the message stream. Sent lines have the shape
|
|
52
|
+
`[timestamp] from -> to: body` (broadcast rows render `broadcast` via the
|
|
53
|
+
`via_broadcast` marker); in follow mode, an inbox drain emits a read line
|
|
54
|
+
(`[timestamp] ${reader} read a message from ${sender}: preview` — the
|
|
55
|
+
reader is the recipient draining its inbox). The diff logic is
|
|
56
|
+
`chatTailDiff()`/`formatChatTailEvent()` in `src/chat.ts`, unit-tested
|
|
57
|
+
there; `--once` prints one snapshot and exits, because a snapshot has no
|
|
58
|
+
previous state to diff read events against.
|
|
59
|
+
|
|
44
60
|
## Environment variables
|
|
45
61
|
|
|
46
62
|
| Variable | Purpose | Default |
|
|
@@ -59,7 +75,7 @@ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none fou
|
|
|
59
75
|
|
|
60
76
|
## Source files
|
|
61
77
|
|
|
62
|
-
- `bin/thatch` — the CLI (Bun script
|
|
78
|
+
- `bin/thatch` — the CLI (Bun script)
|
|
63
79
|
- `bin/release` — release helper (bash, separate from the main CLI)
|
|
64
80
|
|
|
65
81
|
## Interactions with other features
|
|
@@ -70,6 +86,8 @@ Inherits stdio, exits with the child's exit code. Errors and exits 1 if none fou
|
|
|
70
86
|
- Extraction ([extraction.md](extraction.md)): `buffer-batch`, `buffer-tool`, `flush-tools` subcommands
|
|
71
87
|
- Hygiene ([hygiene.md](hygiene.md)): `hygiene`, `reminder` subcommands
|
|
72
88
|
- Multi-host ([multi-host.md](multi-host.md)): `mcp` subcommand starts the MCP server for Claude Code and Cursor
|
|
89
|
+
- Session archaeology: the `session` subcommand group (see the table above) reads the opencode session database via `src/session-db.ts`
|
|
90
|
+
- Cross-session chat ([cross-session-chat.md](cross-session-chat.md)): the `chat` subcommand group reads the shared chat directory and inbox
|
|
73
91
|
|
|
74
92
|
## Key invariants
|
|
75
93
|
|