@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.
Files changed (40) hide show
  1. package/README.md +10 -3
  2. package/artifacts/skills/common.md +4 -0
  3. package/artifacts/skills/thatch-code-review.md +10 -4
  4. package/artifacts/skills/thatch-review-synthesizer.md +57 -6
  5. package/artifacts/skills/thatch-review-technique.md +173 -0
  6. package/bin/thatch +160 -16
  7. package/docs/dev/README.md +26 -14
  8. package/docs/dev/features/README.md +1 -0
  9. package/docs/dev/features/behavior-engine.md +5 -3
  10. package/docs/dev/features/cli.md +22 -4
  11. package/docs/dev/features/cross-session-chat.md +256 -0
  12. package/docs/dev/features/database.md +52 -4
  13. package/docs/dev/features/memory-store.md +13 -0
  14. package/docs/dev/features/multi-host.md +17 -9
  15. package/docs/dev/features/prediction-engine.md +6 -3
  16. package/docs/dev/features/setup.md +12 -5
  17. package/docs/dev/gotchas.md +7 -4
  18. package/docs/dev/mcp-parity.md +2 -2
  19. package/docs/dev/setup-and-hooks.md +5 -4
  20. package/docs/dev/skills.md +7 -3
  21. package/docs/user/README.md +24 -4
  22. package/docs/user/cli.md +30 -0
  23. package/docs/user/code-review.md +4 -3
  24. package/docs/user/cross-session-chat.md +102 -0
  25. package/docs/user/memory.md +3 -0
  26. package/docs/user/setup.md +20 -4
  27. package/docs/user/skills.md +7 -5
  28. package/package.json +2 -1
  29. package/src/behavior.ts +1 -1
  30. package/src/chat-names.ts +121 -0
  31. package/src/chat.ts +879 -0
  32. package/src/db.ts +181 -2
  33. package/src/embeddings.ts +152 -9
  34. package/src/index.ts +113 -5
  35. package/src/prediction.ts +1 -1
  36. package/src/prompts.ts +133 -8
  37. package/src/scoring-engine.ts +25 -4
  38. package/src/setup.ts +83 -29
  39. package/src/skills.ts +47 -16
  40. 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
- const reminder = claudeSessionStartReminder(repo, hygiene);
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
- const skillsDir = result.skills[0]?.path
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
- const skillsDir = result.skills[0]?.path
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 {
@@ -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
- └── watchers.ts → in-memory watcher registry + poller (opencode-only,
44
- event-driven PR notifications delivered as prompts)
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 gate; 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 (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`, and cancels the session's watchers. `session.compacted`: clears the compacting flag so `chat.message` nudges resume. |
100
- | `dispose` | Stops the version checker and the watcher poller, closes the DB. |
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), `~/.claude/skills/`
123
- (Claude Code), or `~/.cursor/skills/` (Cursor) — never into the
124
- worktree; drifted content is overwritten on plugin init or re-running
125
- `thatch setup`. Skills are split into `SHARED_SKILLS` (work on all
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-wide search for a near-identical behavior
82
- (cosine >= 0.85). If found, links this matcher to it via an edge rather
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
@@ -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 setup installer.
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, 486 lines)
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