@jeffober/thatch 0.1.44 → 0.1.46
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 +87 -14
- package/bin/thatch +44 -9
- package/docs/dev/README.md +4 -2
- package/docs/dev/features/README.md +1 -0
- package/docs/dev/features/cicd.md +14 -5
- package/docs/dev/features/commands.md +74 -0
- package/docs/dev/features/cross-session-chat.md +24 -10
- package/docs/dev/features/extraction.md +5 -0
- package/docs/dev/gotchas.md +8 -0
- package/docs/dev/mcp-parity.md +1 -0
- package/docs/dev/setup-and-hooks.md +3 -1
- package/docs/plans/thatch-actions.md +55 -20
- package/docs/user/README.md +5 -1
- package/docs/user/cli.md +12 -3
- package/docs/user/commands.md +50 -0
- package/docs/user/cross-session-chat.md +11 -7
- package/docs/user/setup.md +9 -3
- package/package.json +1 -1
- package/src/chat.ts +32 -4
- package/src/commands.ts +122 -13
- package/src/db.ts +9 -0
- package/src/embeddings.ts +42 -1
- package/src/index.ts +141 -32
- package/src/mcp.ts +62 -3
- package/src/prompts.ts +55 -1
- package/src/setup.ts +9 -0
- package/src/tool-defs.ts +19 -8
- package/docs/plans/nomenclater.md +0 -89
package/README.md
CHANGED
|
@@ -51,6 +51,54 @@ thatch setup --claude --global # or --cursor --global
|
|
|
51
51
|
Restart your editor and thatch's tools are available as `mcp__thatch__*`.
|
|
52
52
|
Requires [Bun] on PATH.
|
|
53
53
|
|
|
54
|
+
The embedding model downloads once at first use. It is cached in the
|
|
55
|
+
platform's per-user cache dir: `~/Library/Caches/thatch/models` on macOS (or
|
|
56
|
+
`$XDG_CACHE_HOME/thatch/models` when `XDG_CACHE_HOME` is set),
|
|
57
|
+
`$XDG_CACHE_HOME/thatch/models` (default `~/.cache/thatch/models`) elsewhere.
|
|
58
|
+
Set `THATCH_MODEL_CACHE` to override the location. The cache survives
|
|
59
|
+
upgrades and works from read-only installs.
|
|
60
|
+
|
|
61
|
+
### NixOS / Nix
|
|
62
|
+
|
|
63
|
+
This repo is a flake. The package bundles Bun and the native embedding
|
|
64
|
+
runtime, so you need no global `npm install` and no `bun` on PATH.
|
|
65
|
+
|
|
66
|
+
Try it without installing:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
nix run github:sysread/thatch -- --version
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Install options:
|
|
73
|
+
|
|
74
|
+
- **NixOS module** (system-wide, all users):
|
|
75
|
+
|
|
76
|
+
```nix
|
|
77
|
+
{
|
|
78
|
+
inputs.thatch.url = "github:sysread/thatch";
|
|
79
|
+
|
|
80
|
+
# in your NixOS system's modules:
|
|
81
|
+
imports = [ inputs.thatch.nixosModules.default ];
|
|
82
|
+
programs.thatch.enable = true; # puts `thatch` on PATH for every user
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- **Overlay**: add `nixpkgs.overlays = [ inputs.thatch.overlays.default ];`,
|
|
87
|
+
then `environment.systemPackages = [ pkgs.thatch ];`
|
|
88
|
+
- **Package directly**: add `inputs.thatch.packages.${system}.default` to a
|
|
89
|
+
`home.packages` or `environment.systemPackages` list
|
|
90
|
+
|
|
91
|
+
Once `thatch` is on PATH, wire it into your editor as usual:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
cd /path/to/your/project
|
|
95
|
+
thatch setup --claude --global # or --cursor
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The embedding model still downloads once at first use. Its cache lives in the
|
|
99
|
+
per-user cache dir described above, outside the read-only Nix store, so it
|
|
100
|
+
survives upgrades. `THATCH_MODEL_CACHE` overrides the location.
|
|
101
|
+
|
|
54
102
|
### Other MCP-compatible harnesses
|
|
55
103
|
|
|
56
104
|
```json
|
|
@@ -62,7 +110,7 @@ Requires [Bun] on PATH.
|
|
|
62
110
|
```
|
|
63
111
|
|
|
64
112
|
Include thatch's instructions in your agent's system prompt manually; see
|
|
65
|
-
[
|
|
113
|
+
[MCP parity](https://github.com/sysread/thatch/wiki/Mcp-Parity) for the prompt text.
|
|
66
114
|
|
|
67
115
|
## What's inside
|
|
68
116
|
|
|
@@ -108,13 +156,17 @@ Thatch gives your agent:
|
|
|
108
156
|
hostile message cannot impersonate your instructions. Disable the whole
|
|
109
157
|
feature with `chat.enabled: false` in the thatch config (or just the
|
|
110
158
|
auto-joining with `chat.autoRegister: false`).
|
|
111
|
-
- **
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
159
|
+
- **Slash commands** -- `/thatch/defrag` (consolidate duplicate memories),
|
|
160
|
+
`/thatch/extract` (drain the extraction queue now), `/thatch/hygiene`
|
|
161
|
+
(tend stale and orphaned memories), and `/thatch/reflect` (persist what
|
|
162
|
+
the session learned) run on demand the same behaviors the nudges run on
|
|
163
|
+
their own schedule. Plus the opencode-only wrap-ups: `/thatch/compact` and
|
|
164
|
+
`/thatch/exit` run a pre-flight checklist before a compaction or an exit:
|
|
165
|
+
the agent flushes pending fact extraction, finishes promised memory
|
|
166
|
+
writes, and surfaces todos or follow-ups it never addressed. It ends its
|
|
167
|
+
response with a greenlight token only when the checklist is clean; thatch
|
|
168
|
+
then triggers the compaction or quits opencode. With items outstanding,
|
|
169
|
+
the agent lists them and nothing fires -- you decide when to retry.
|
|
118
170
|
- **Notifications + user config** -- the agent can ping you out-of-band when a
|
|
119
171
|
long-running outcome lands: a desktop banner, a spoken voice
|
|
120
172
|
announcement, or both (macOS and Linux). Preferences live in a
|
|
@@ -127,10 +179,28 @@ review, review response, change and feature walkthroughs, memory verification,
|
|
|
127
179
|
knowledge export, and writing tasks
|
|
128
180
|
(PR descriptions, tickets, PR splitting).
|
|
129
181
|
|
|
130
|
-
See the [user guide](
|
|
182
|
+
See the [user guide](https://github.com/sysread/thatch/wiki/Guide%3A-Overview) for the full tool list, CLI
|
|
131
183
|
commands, configuration, environment variables, and detailed setup for each
|
|
132
184
|
host.
|
|
133
185
|
|
|
186
|
+
## What works in Claude Code
|
|
187
|
+
|
|
188
|
+
The memory tools, prediction and behavior engines, conversation search via
|
|
189
|
+
the `thatch session` CLI, cross-session chat (with stop-hook wake), skills,
|
|
190
|
+
and the on-demand actions `/thatch/defrag`, `/thatch/hygiene`, and
|
|
191
|
+
`/thatch/reflect` (synced by `thatch setup --claude`). Not available: the
|
|
192
|
+
wrap-up commands and `/thatch/extract` (they need the opencode plugin's
|
|
193
|
+
session identity and TUI control routes), live recall/prediction nudges
|
|
194
|
+
beyond the hook-based flow, and toast notifications.
|
|
195
|
+
|
|
196
|
+
## What works in Cursor
|
|
197
|
+
|
|
198
|
+
The same core set as Claude Code (memory, predictions, behaviors, chat with
|
|
199
|
+
stop-hook wake, skills, and the `defrag`/`hygiene`/`reflect` actions). Cursor
|
|
200
|
+
has no file-based slash commands, so the actions surface through MCP prompts
|
|
201
|
+
instead. Not available: the wrap-up commands, `/thatch/extract`, and toast
|
|
202
|
+
notifications.
|
|
203
|
+
|
|
134
204
|
## Privacy
|
|
135
205
|
|
|
136
206
|
Everything is local. The embedding model downloads once from Hugging Face Hub
|
|
@@ -152,16 +222,19 @@ bun install
|
|
|
152
222
|
mise run check # typecheck + bun test + markdownlint (the CI gate)
|
|
153
223
|
```
|
|
154
224
|
|
|
225
|
+
On Nix, `nix develop` drops you into a shell with bun, mise, and node already
|
|
226
|
+
on PATH -- no system install needed.
|
|
227
|
+
|
|
155
228
|
Tests never reach outside the sandbox: temp-directory SQLite files, mock
|
|
156
229
|
embeddings, no network.
|
|
157
230
|
|
|
158
231
|
## Docs
|
|
159
232
|
|
|
160
|
-
- [User guide](
|
|
161
|
-
- [Development](
|
|
162
|
-
- [MCP parity](
|
|
163
|
-
- [QA tests](tests/qa
|
|
164
|
-
- [Design docs](docs/plans
|
|
233
|
+
- [User guide](https://github.com/sysread/thatch/wiki/Guide%3A-Overview) -- setup, tools, configuration, CLI
|
|
234
|
+
- [Development](https://github.com/sysread/thatch/wiki/Developer-Guide) -- architecture, module responsibilities
|
|
235
|
+
- [MCP parity](https://github.com/sysread/thatch/wiki/Mcp-Parity) -- OpenCode plugin vs MCP feature comparison
|
|
236
|
+
- [QA tests](https://github.com/sysread/thatch/tree/main/tests/qa) -- executable use cases (auto/ and live/ subdirs)
|
|
237
|
+
- [Design docs](https://github.com/sysread/thatch/tree/main/docs/plans) -- design-decision snapshots and in-progress plans
|
|
165
238
|
|
|
166
239
|
## License
|
|
167
240
|
|
package/bin/thatch
CHANGED
|
@@ -10,7 +10,7 @@ import { hygieneReport } from "../src/hygiene";
|
|
|
10
10
|
import { claudeSessionStartReminder } from "../src/prompts";
|
|
11
11
|
import { setupClaudeCode, setupCursor, registerClaudeMcpServer } from "../src/setup";
|
|
12
12
|
import { resolveOpencodeDbPath, SessionDB, partToTimelineEntry, partToTranscriptEntries, partToFullJson, messageToFullJson, type SessionPart } from "../src/session-db";
|
|
13
|
-
import { chatTailDiff, chatTailBacklog, filterChatTailRows, formatChatTailJsonl, CHAT_TAIL_DEFAULT_LIMIT, parseChatTimeBound, CHAT_STALE_MS, CHAT_RENUDGE_MINUTES, chatLiveness, humanAge, splitChatRoster, mcpSessionID, isoMinutesAgo, renderChatParticipant, resolveRegisteredPredecessor, type ChatSessionRow, type ChatTailFilter } from "../src/chat";
|
|
13
|
+
import { chatTailDiff, chatTailBacklog, filterChatTailRows, formatChatTailJsonl, CHAT_TAIL_DEFAULT_LIMIT, parseChatTimeBound, CHAT_STALE_MS, CHAT_RENUDGE_MINUTES, chatLiveness, humanAge, splitChatRoster, sortChatRoster, mcpSessionID, isoMinutesAgo, renderChatParticipant, resolveRegisteredPredecessor, type ChatSessionRow, type ChatTailFilter } from "../src/chat";
|
|
14
14
|
import { chatEnabled, loadConfig } from "../src/config";
|
|
15
15
|
import { chatNotificationNudge } from "../src/prompts";
|
|
16
16
|
import { createDebugLog } from "../src/debug";
|
|
@@ -49,8 +49,11 @@ function usage(): never {
|
|
|
49
49
|
Full conversation as OpenAI chat-completions JSONL
|
|
50
50
|
thatch session search <query> [--session <id>] [--regex] [--limit N]
|
|
51
51
|
JSONL of matching parts across sessions (decoded content)
|
|
52
|
-
thatch chat list
|
|
53
|
-
|
|
52
|
+
thatch chat list [--stale N|all] Registered chat sessions, grouped Active/Stale:
|
|
53
|
+
name, age, status, project, topic.
|
|
54
|
+
--stale N (days) hides stale sessions
|
|
55
|
+
older than N; default 1. --stale all
|
|
56
|
+
shows every stale session.
|
|
54
57
|
thatch chat tail [--once] [--limit N|all] [--match RE]
|
|
55
58
|
[--from NAME] [--to NAME] [--since DT] [--until DT]
|
|
56
59
|
Follow cross-session chat as a JSONL
|
|
@@ -412,14 +415,17 @@ const CHAT_ROSTER_HEADER = ["NAME", "AGE", "STATUS", "PROJECT", "TOPIC"];
|
|
|
412
415
|
* an aligned-column table under a header row. Section headers are bold,
|
|
413
416
|
* the stale explainer is muted italic; rows use green names, dim age,
|
|
414
417
|
* green/red status, and a muted italic topic.
|
|
418
|
+
* staleCapMs bounds how old a stale row is displayed (null = no bound);
|
|
419
|
+
* rows past the cap collapse into a muted hidden-count note so the reader
|
|
420
|
+
* knows rows exist that the window left out.
|
|
415
421
|
* Color is applied only when stdout is an interactive terminal (and
|
|
416
422
|
* NO_COLOR is unset), so piped output - grep, redirect, QA runners - stays
|
|
417
423
|
* clean plain text.
|
|
418
424
|
*/
|
|
419
|
-
function formatChatRoster(sessions: ChatSessionRow[]): string {
|
|
425
|
+
function formatChatRoster(sessions: ChatSessionRow[], staleCapMs: number | null = null): string {
|
|
420
426
|
const colored = process.stdout.isTTY === true && !process.env.NO_COLOR;
|
|
421
427
|
const paint = (code: string, s: string) => (colored ? `\x1b[${code}m${s}\x1b[0m` : s);
|
|
422
|
-
const { active, stale } = splitChatRoster(sessions);
|
|
428
|
+
const { active, stale, staleHidden } = splitChatRoster(sortChatRoster(sessions), Date.now(), staleCapMs);
|
|
423
429
|
const renderTable = (rows: ChatSessionRow[]) => {
|
|
424
430
|
const cells = (r: ChatSessionRow) => [
|
|
425
431
|
r.name,
|
|
@@ -449,9 +455,21 @@ function formatChatRoster(sessions: ChatSessionRow[]): string {
|
|
|
449
455
|
if (active.length > 0) {
|
|
450
456
|
parts.push([paint("1", "# Active Sessions"), renderTable(active)].join("\n"));
|
|
451
457
|
}
|
|
452
|
-
if (stale.length > 0) {
|
|
453
|
-
const
|
|
454
|
-
|
|
458
|
+
if (stale.length > 0 || staleHidden > 0) {
|
|
459
|
+
const lines = [paint("1", "# Stale Sessions")];
|
|
460
|
+
if (stale.length > 0) {
|
|
461
|
+
const explainer = `These sessions have missed their heartbeat for over ${Math.round(CHAT_STALE_MS / 1000)} seconds. Their harnesses may no longer be running (stopped, or the machine slept); mail waits until they resume.`;
|
|
462
|
+
lines.push(paint("3;90", explainer), renderTable(stale));
|
|
463
|
+
}
|
|
464
|
+
if (staleHidden > 0) {
|
|
465
|
+
lines.push(
|
|
466
|
+
paint(
|
|
467
|
+
"3;90",
|
|
468
|
+
`${staleHidden} older stale session${staleHidden === 1 ? "" : "s"} hidden; --stale N widens the window to N days, --stale all shows everything.`,
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
parts.push(lines.join("\n"));
|
|
455
473
|
}
|
|
456
474
|
return parts.join("\n\n");
|
|
457
475
|
}
|
|
@@ -461,12 +479,26 @@ async function chatCommand(rest: string[]): Promise<void> {
|
|
|
461
479
|
|
|
462
480
|
switch (sub) {
|
|
463
481
|
case "list": {
|
|
482
|
+
// --stale N bounds how old a stale row the roster displays, in days
|
|
483
|
+
// (fractions allowed); the default of one day keeps long-dead
|
|
484
|
+
// sessions from burying the fresh signal. --stale all restores the
|
|
485
|
+
// unbounded view. A bad value dies loudly rather than silently
|
|
486
|
+
// shrinking the roster.
|
|
487
|
+
const staleArg = flagValue(restArgs, "--stale");
|
|
488
|
+
let staleCapMs: number | null = 24 * 60 * 60 * 1000;
|
|
489
|
+
if (staleArg !== undefined) {
|
|
490
|
+
if (staleArg === "all") staleCapMs = null;
|
|
491
|
+
else if (!/^\d+(\.\d+)?$/.test(staleArg) || Number(staleArg) <= 0) {
|
|
492
|
+
console.error(`error: --stale expects a positive number of days or "all", got "${staleArg}"`);
|
|
493
|
+
process.exit(1);
|
|
494
|
+
} else staleCapMs = Number(staleArg) * 24 * 60 * 60 * 1000;
|
|
495
|
+
}
|
|
464
496
|
const sessions = db.listChatSessions();
|
|
465
497
|
if (sessions.length === 0) {
|
|
466
498
|
console.log("No sessions registered. Sessions join the directory automatically on their first idle (opencode), or via chat_register.");
|
|
467
499
|
break;
|
|
468
500
|
}
|
|
469
|
-
console.log(formatChatRoster(sessions));
|
|
501
|
+
console.log(formatChatRoster(sessions, staleCapMs));
|
|
470
502
|
break;
|
|
471
503
|
}
|
|
472
504
|
|
|
@@ -831,6 +863,9 @@ switch (command) {
|
|
|
831
863
|
console.log(` Instructions: ${result.claudeMd}`);
|
|
832
864
|
console.log(` Hooks: ${result.settings}`);
|
|
833
865
|
printSkills(result);
|
|
866
|
+
if (result.commands.length > 0) {
|
|
867
|
+
console.log(` Commands: ${result.commands.length} written (${result.commands.map((p) => p.split("/").pop()).join(", ")})`);
|
|
868
|
+
}
|
|
834
869
|
console.log();
|
|
835
870
|
if (result.mcpAddCommand) {
|
|
836
871
|
// Global setup cannot write ~/.claude.json directly, so run the
|
package/docs/dev/README.md
CHANGED
|
@@ -24,7 +24,8 @@ Shared core
|
|
|
24
24
|
├── embeddings.ts → embedding model via transformers.js
|
|
25
25
|
├── git.ts → detect repo identity (store name)
|
|
26
26
|
├── hygiene.ts → hygiene report (pending dedups, stale, orphaned branches)
|
|
27
|
-
├── prompts.ts → system prompt, compaction, reminders, recall/prediction/behavior nudges, CLAUDE.md instructions
|
|
27
|
+
├── prompts.ts → system prompt, compaction, reminders, recall/prediction/behavior nudges, prompt cores, CLAUDE.md instructions
|
|
28
|
+
├── commands.ts → /thatch slash-command registry: actions from prompt cores + wrap-ups, per-host sync
|
|
28
29
|
├── sideband.ts → Unix socket server + client: warm-model semantic match for hook processes
|
|
29
30
|
├── skills.ts → SKILL.md content + installer (shared + opencode-only arrays)
|
|
30
31
|
├── scoring-engine.ts → generic four-table scoring base (shared by prediction + behavior engines)
|
|
@@ -83,7 +84,8 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
83
84
|
| `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
85
|
| `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. The pool is the fallback base for registrations with no usable session title. |
|
|
85
86
|
| `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. |
|
|
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. |
|
|
87
|
+
| `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. Also the prompt cores (`defragCore`, `hygieneCore`, `reflectCore`, `extractionCore`) - host-agnostic behavior instruction bodies shared by nudges and `/thatch` commands, parameterized by a `ToolNamer` for per-host tool spellings. See [features/commands.md](features/commands.md). |
|
|
88
|
+
| `commands.ts` | Slash-command registry and sync. `actionDefs(tool)` renders the on-demand actions (defrag, extract, hygiene, reflect) from the prompt cores; the wrap-up commands (compact, exit) are bespoke greenlight-gated templates. Renderers produce full command files for opencode (`installOpencodeCommands`, synced by the plugin at init) and Claude Code (`installClaudeCommands`, synced by setup); `compilePrompts` in `mcp.ts` exposes the same actions as MCP prompts for Cursor. A parity-guard test pins the three sets to the documented exclusions. See [features/commands.md](features/commands.md). |
|
|
87
89
|
| `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]`. |
|
|
88
90
|
| `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. |
|
|
89
91
|
| `prediction.ts` | Thin wrapper around `ScoringEngine` with prediction-specific table names. |
|
|
@@ -23,6 +23,7 @@ system, see [../skills.md](../skills.md). For non-obvious invariants, see
|
|
|
23
23
|
| [behavior-engine.md](behavior-engine.md) | Behavior engine (LLM self-discipline rules, ham/spam, auto-fire) |
|
|
24
24
|
| [hygiene.md](hygiene.md) | Hygiene system (duplicate, stale, orphaned branch signals) |
|
|
25
25
|
| [deduplication.md](deduplication.md) | Deduplication system (find pairs, verdict lifecycle) |
|
|
26
|
+
| [commands.md](commands.md) | Slash commands (on-demand actions from shared prompt cores, wrap-ups, host sync) |
|
|
26
27
|
| [sideband.md](sideband.md) | Sideband IPC (Unix socket for MCP host hook processes) |
|
|
27
28
|
| [compaction-recovery.md](compaction-recovery.md) | Compaction recovery (opencode compaction hooks) |
|
|
28
29
|
| [session-lifecycle.md](session-lifecycle.md) | Session lifecycle management (opencode events, child tracking, wrap-up commands) |
|
|
@@ -5,7 +5,7 @@ Thatch uses GitHub Actions for CI and publishing, plus a local release helper sc
|
|
|
5
5
|
## CI pipeline (.github/workflows/ci.yml)
|
|
6
6
|
|
|
7
7
|
Runs on every push/PR to main. Three jobs:
|
|
8
|
-
1. **Typecheck** — `bunx tsc -p tsconfig.check.json` (includes
|
|
8
|
+
1. **Typecheck** — `bunx tsc -p tsconfig.check.json` (includes src, tests, and the CI scripts in `.github/scripts`; the build's `tsconfig.json` includes only src)
|
|
9
9
|
2. **Tests** — `bun test` (bun:test framework, no network, `:memory:` DB or temp dirs)
|
|
10
10
|
3. **Markdownlint** — `bunx markdownlint-cli2` on `README.md` and `docs/**/*.md` (excluding `docs/plans/**`)
|
|
11
11
|
|
|
@@ -22,6 +22,14 @@ Key details:
|
|
|
22
22
|
- npm 12 shipped a sigstore bug that broke OIDC publishing; the pin prevents it
|
|
23
23
|
- The workflow deliberately sets NO `registry-url`, which would write a token-expecting `.npmrc` that preempts the OIDC exchange
|
|
24
24
|
|
|
25
|
+
## Wiki pipeline (.github/workflows/wiki.yml)
|
|
26
|
+
|
|
27
|
+
Triggered by pushes to main that touch `docs/**`, the workflow file, or the render script. One-way mirror of `docs/` into the GitHub wiki: renders each doc to a flat wiki page (tiered prefixes `Guide:`, bare, `Feature:`; see `.github/scripts/sync-wiki.ts`), rewrites relative md links, generates `Home.md` + `_Sidebar.md`, wipes and rewrites all wiki pages, then pushes to `sysread/thatch.wiki.git`.
|
|
28
|
+
|
|
29
|
+
- Uses the `WIKI_TOKEN` Actions secret (classic PAT, `public_repo` scope) because the built-in `GITHUB_TOKEN` cannot push to wiki repos
|
|
30
|
+
- Precondition: the wiki must be initialized once via the UI (GitHub does not auto-create `.wiki.git` repos)
|
|
31
|
+
- `docs/plans/` and `docs/in-progress/` are excluded; duplicate or reserved page names fail the render script
|
|
32
|
+
|
|
25
33
|
## Release helper (bin/release)
|
|
26
34
|
|
|
27
35
|
A bash script (`set -euo pipefail`) that bumps, checks, commits, tags, and pushes.
|
|
@@ -45,7 +53,7 @@ Steps:
|
|
|
45
53
|
| `test` | Run the test suite (`bun test`) |
|
|
46
54
|
| `test-watch` | Run tests in watch mode |
|
|
47
55
|
| `coverage` | Run tests with coverage report |
|
|
48
|
-
| `typecheck` | Typecheck src + tests (`bunx tsc -p tsconfig.check.json`) |
|
|
56
|
+
| `typecheck` | Typecheck src + tests + `.github/scripts` (`bunx tsc -p tsconfig.check.json`) |
|
|
49
57
|
| `lint-md` | Lint markdown docs (`bunx markdownlint-cli2`) |
|
|
50
58
|
| `check` | The CI gate: typecheck + tests + markdownlint |
|
|
51
59
|
| `release` | Bump version, commit, tag, push (args: `patch\|minor\|major`) |
|
|
@@ -60,7 +68,7 @@ Steps:
|
|
|
60
68
|
## Development tooling
|
|
61
69
|
|
|
62
70
|
- **Bun 1.3.14** (pinned via mise)
|
|
63
|
-
- **TypeScript** with `tsconfig.check.json` for typecheck (includes
|
|
71
|
+
- **TypeScript** with `tsconfig.check.json` for typecheck (includes src, tests, and `.github/scripts`; build's `tsconfig.json` includes only src)
|
|
64
72
|
- **bun:test** framework (no network, `:memory:` DB or temp dirs, mock embeddings)
|
|
65
73
|
- **markdownlint-cli2** with custom config (`.markdownlint-cli2.jsonc`): disables MD013 (line length), MD032 (blanks around lists), MD036 (emphasis as heading), MD060 (table alignment). Keeps MD040 (code-block language), MD047 (trailing newline), heading hierarchy.
|
|
66
74
|
- **Zod** for tool schema validation and JSON Schema generation (`src/tool-defs.ts`)
|
|
@@ -74,10 +82,11 @@ Steps:
|
|
|
74
82
|
|
|
75
83
|
- `.github/workflows/ci.yml` — CI pipeline
|
|
76
84
|
- `.github/workflows/publish.yml` — publish pipeline (OIDC)
|
|
85
|
+
- `.github/workflows/wiki.yml` + `.github/scripts/sync-wiki.ts` — wiki mirror pipeline
|
|
77
86
|
- `bin/release` — release helper script
|
|
78
87
|
- `mise.toml` — task definitions, tool pins, env vars
|
|
79
88
|
- `.markdownlint-cli2.jsonc` — markdownlint config
|
|
80
|
-
- `tsconfig.check.json` — typecheck config (includes tests)
|
|
89
|
+
- `tsconfig.check.json` — typecheck config (includes src, tests, `.github/scripts`)
|
|
81
90
|
|
|
82
91
|
## Key invariants
|
|
83
92
|
|
|
@@ -86,4 +95,4 @@ Steps:
|
|
|
86
95
|
- npm is pinned to 11.x. npm 12 shipped a sigstore bug that broke OIDC publishing.
|
|
87
96
|
- The release helper checks for tagged-but-not-published state (tag exists, npm returns E404) and refuses to proceed with recovery instructions.
|
|
88
97
|
- CI runs on push/PR to main. Publish runs only on `v*` tags. The release helper bridges them: tag + push triggers publish.
|
|
89
|
-
- `tsconfig.check.json` includes
|
|
98
|
+
- `tsconfig.check.json` includes src, tests, and `.github/scripts` for typecheck. The build's `tsconfig.json` includes only src.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Slash commands: actions and wrap-ups
|
|
2
|
+
|
|
3
|
+
Thatch's `/thatch/*` commands come in two families:
|
|
4
|
+
|
|
5
|
+
- **Actions** (`defrag`, `extract`, `hygiene`, `reflect`) — user-invoked
|
|
6
|
+
wrappers around behaviors the nudges otherwise run on their own schedule.
|
|
7
|
+
- **Wrap-ups** (`compact`, `exit`) — greenlight-gated pre-compaction and
|
|
8
|
+
pre-exit checklists. opencode only: the greenlight check needs the
|
|
9
|
+
plugin's `command.execute.before` arming and TUI control routes.
|
|
10
|
+
|
|
11
|
+
User-facing guide: [../../user/commands.md](../../user/commands.md).
|
|
12
|
+
|
|
13
|
+
## Prompt cores
|
|
14
|
+
|
|
15
|
+
Action bodies are **prompt cores**: constants in `src/prompts.ts`
|
|
16
|
+
(`defragCore`, `hygieneCore`, `reflectCore`, `extractionCore`) that hold the
|
|
17
|
+
host-agnostic instruction text for one behavior. The nudge envelopes wrap
|
|
18
|
+
the same cores with escalation tiers and the stop-and-wait ending; command
|
|
19
|
+
and prompt renderers wrap them with frontmatter and a user-message section.
|
|
20
|
+
The point is drift-proofing: a wording change lands in the nudge and the
|
|
21
|
+
command at once, and a test asserts each rendered file contains its core
|
|
22
|
+
verbatim.
|
|
23
|
+
|
|
24
|
+
The stop-and-wait ending ("do not advance pending work on this nudge's
|
|
25
|
+
account") is nudge-only by design. A `/thatch` command IS user input, so
|
|
26
|
+
the command envelope must not inherit it.
|
|
27
|
+
|
|
28
|
+
Cores that name memory tools take a `ToolNamer` because spellings differ
|
|
29
|
+
per host: `thatch_find_duplicates` on opencode, `mcp__thatch__find_duplicates`
|
|
30
|
+
on MCP hosts. Each host's renderer passes its own namer
|
|
31
|
+
(`opencodeToolName` / `mcpToolName`).
|
|
32
|
+
|
|
33
|
+
## Rendering and sync
|
|
34
|
+
|
|
35
|
+
`src/commands.ts` is the single registry:
|
|
36
|
+
|
|
37
|
+
- `actionDefs(tool)` returns the action set with bodies rendered for one
|
|
38
|
+
host. Actions needing plugin-only capabilities are marked `opencodeOnly`
|
|
39
|
+
(`extract`: needs session identity via `get_session_info`).
|
|
40
|
+
- `opencodeCommandDefs()` = wrap-up templates + all actions. Synced by the
|
|
41
|
+
plugin at init into `$XDG_CONFIG_HOME/opencode/command/thatch/`
|
|
42
|
+
(`installOpencodeCommands`), content-compare, self-healing.
|
|
43
|
+
- `claudeCommandDefs()` = actions minus `opencodeOnly`. Synced by
|
|
44
|
+
`thatch setup --claude` (`installClaudeCommands`) into
|
|
45
|
+
`<claudeDir>/commands/thatch/`, reported in the setup output. There is no
|
|
46
|
+
plugin on this host to sync on load, so setup is the sync point.
|
|
47
|
+
- MCP prompts: `compilePrompts()` in `src/mcp.ts` exposes the same
|
|
48
|
+
non-opencodeOnly actions through `prompts/list` / `prompts/get`, which
|
|
49
|
+
Cursor surfaces as slash commands. The optional `focus` argument is
|
|
50
|
+
appended to the body as a `Focus:` line, standing in for the file
|
|
51
|
+
commands' `$ARGUMENTS`.
|
|
52
|
+
|
|
53
|
+
A parity-guard test asserts the three sets are the same actions minus the
|
|
54
|
+
documented exclusions: opencode = Claude Code set + `compact`, `exit`,
|
|
55
|
+
`extract`; MCP prompts = Claude Code's set.
|
|
56
|
+
|
|
57
|
+
## The extract action and session IDs
|
|
58
|
+
|
|
59
|
+
`/thatch/extract` runs the extraction flow on demand: learn the session ID
|
|
60
|
+
from `thatch_get_session_info`, dispatch the extractor sub-agent with an
|
|
61
|
+
explicit `session_id`, acknowledge with `thatch_extraction_done`. The
|
|
62
|
+
explicit ID matters because the sub-agent's session differs from the
|
|
63
|
+
parent's — the same reason the extraction nudge passes it.
|
|
64
|
+
|
|
65
|
+
`get_extraction_payload` accepts an omitted `session_id`: on the opencode
|
|
66
|
+
path the wrapper supplies the invoking session's `HostToolContext.sessionID`.
|
|
67
|
+
The zod arg shape is shared between hosts, so optionality cannot be
|
|
68
|
+
per-host at the schema level; on MCP hosts (no session context) an omitted
|
|
69
|
+
`session_id` returns a clear "pass the parent's session_id" error. The rule
|
|
70
|
+
the model applies, stated in the arg descriptions and the dev docs:
|
|
71
|
+
|
|
72
|
+
> Omit `session_id` when acting on the session you are running in. Pass the
|
|
73
|
+
> parent session's `session_id` when running inside a sub-agent dispatched
|
|
74
|
+
> to process another session's extraction queue.
|
|
@@ -58,10 +58,13 @@ outlives a process, so liveness needs a signal beyond process lifetime.
|
|
|
58
58
|
|
|
59
59
|
### Registration and identity
|
|
60
60
|
|
|
61
|
-
Sessions are auto-registered by the plugin:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
Sessions are auto-registered by the plugin: a top-level session's first
|
|
62
|
+
real user message inserts its directory row (the `chat.message` hook,
|
|
63
|
+
which fires before the model runs - so the session is addressable within
|
|
64
|
+
its very first turn), and the first `session.status` idle event remains a
|
|
65
|
+
backstop that also converges the topic once the auto-titler lands. A
|
|
66
|
+
session continued via `-s <id>` or `-c` registers at harness start
|
|
67
|
+
(below). All paths are skipped when `chat.autoRegister: false` or when the
|
|
65
68
|
session left explicitly (a leave tombstone, see below). Names are assigned, never
|
|
66
69
|
claimed: a pool draw (`CHAT_NAME_POOL`, src/chat-names.ts) plus a
|
|
67
70
|
per-base counter row (`chat_name_counters`, src/db.ts) that only ever
|
|
@@ -94,9 +97,14 @@ args=` on macOS) and parses `-s`/`--session` from that.
|
|
|
94
97
|
Registration is keyed by session id: the row already exists, so the name
|
|
95
98
|
is RECLAIMED (names are owned by the session id, minted once, never
|
|
96
99
|
renamed) and the row's heartbeat is refreshed - hosting is the
|
|
97
|
-
heartbeat, so the serving harness beats and delivers for the row again.
|
|
98
|
-
command line
|
|
99
|
-
|
|
100
|
+
heartbeat, so the serving harness beats and delivers for the row again.
|
|
101
|
+
`-c/--continue` resolves no id on the command line, so the plugin
|
|
102
|
+
resolves the same target the harness does: `client.session.list()`
|
|
103
|
+
(directory-scoped, the plugin's client carries the directory), sorted by
|
|
104
|
+
`time.updated` descending like the TUI does, first row without a
|
|
105
|
+
`parentID`. That registration is not synchronous at init (the list call
|
|
106
|
+
is a server round trip), but it lands within a beat of startup - the
|
|
107
|
+
heartbeat is live long before the first prompt. Asleep-mail: the
|
|
100
108
|
startup path kicks a delivery pass when it finishes, so the continued
|
|
101
109
|
session learns what it missed at startup instead of waiting out the
|
|
102
110
|
first poll cycle.
|
|
@@ -118,8 +126,8 @@ publishing `app.exit`, so the roster stops advertising a host that is
|
|
|
118
126
|
about to vanish; the exit template's checklist step asks the model to do
|
|
119
127
|
the same earlier) - writes a
|
|
120
128
|
`chat_leave_tombstones` row in the same transaction that deletes the
|
|
121
|
-
directory row. The tombstone is what makes a leave stick:
|
|
122
|
-
registration paths consult it (the plugin's idle auto-
|
|
129
|
+
directory row. The tombstone is what makes a leave stick: all
|
|
130
|
+
registration paths consult it (the plugin's idle and prompt auto-registerers are
|
|
123
131
|
suppressed outright; the MCP hook's ensure-register prints the leave line
|
|
124
132
|
instead of rejoining), so a leave cannot be silently undone by the next
|
|
125
133
|
idle moment. The gate lives in `ChatStore.register` via `#tombstoneGate`
|
|
@@ -187,7 +195,13 @@ and the broadcast skip all call it, so they cannot disagree. `chat_list`
|
|
|
187
195
|
groups the result into Active and Stale sections - the stale section's
|
|
188
196
|
explainer says what stale means - and `chat_send` states the recipient's
|
|
189
197
|
liveness at send time, so a sender mailing a ghost learns it immediately
|
|
190
|
-
instead of waiting on a wake that will never fire.
|
|
198
|
+
instead of waiting on a wake that will never fire. Both roster surfaces
|
|
199
|
+
order rows with `sortChatRoster` (also src/chat.ts): project first,
|
|
200
|
+
alphabetical, with no-project rows last; most recently seen first within
|
|
201
|
+
a project. The CLI roster bounds
|
|
202
|
+
the stale display to one day by default (`--stale N` in days, `--stale all`
|
|
203
|
+
unbounded), collapsing older rows into a hidden-count note; the tool
|
|
204
|
+
surface stays unbounded so an agent always sees the full directory.
|
|
191
205
|
`session.deleted` is the graceful-exit fast path that unregisters immediately.
|
|
192
206
|
|
|
193
207
|
### Delivery, re-nudge, and the rate cap
|
|
@@ -197,6 +197,11 @@ continues working while the child extracts.
|
|
|
197
197
|
- **opencode**: peeks accepted + pending interactions, builds JSON via
|
|
198
198
|
`buildExtractionPayload`
|
|
199
199
|
- **MCP**: peeks the file-backed queue, builds the same JSON payload
|
|
200
|
+
- `session_id` is optional: omitted, it resolves to the invoking session
|
|
201
|
+
via `HostToolContext.sessionID` (opencode path). On MCP hosts there is no
|
|
202
|
+
session context, so an omitted `session_id` returns a "pass the parent
|
|
203
|
+
session's session_id" error. Explicit IDs always win - that is how a
|
|
204
|
+
sub-agent drains the parent's queue
|
|
200
205
|
- Returns `null` when no interactions are queued
|
|
201
206
|
- Read-only — it peeks the queue; it does not consume it. Consumption is
|
|
202
207
|
`extraction_done`'s job
|
package/docs/dev/gotchas.md
CHANGED
|
@@ -146,6 +146,14 @@ here first. These are the things that have already cost time.
|
|
|
146
146
|
- **The binary path is baked into installed hook commands.** `thatch setup`
|
|
147
147
|
resolves `<bin>` from PATH (or the script's absolute path) and writes it into
|
|
148
148
|
every hook command, so hooks survive after the setup session ends.
|
|
149
|
+
- **Tool-arg optionality cannot be per-host at the schema level.** The zod
|
|
150
|
+
arg shape in `TOOL_DEFS` is shared by the opencode plugin and the MCP
|
|
151
|
+
server, so making an arg optional (e.g. `get_extraction_payload`'s
|
|
152
|
+
`session_id`) makes it optional everywhere. Per-host enforcement has to
|
|
153
|
+
happen in the tool's `execute`: fall back to `HostToolContext.sessionID`
|
|
154
|
+
on the opencode path, return a "pass the parent's session_id" error on
|
|
155
|
+
MCP hosts, which have no session context. See
|
|
156
|
+
[features/commands.md](features/commands.md).
|
|
149
157
|
|
|
150
158
|
## Tests
|
|
151
159
|
|
package/docs/dev/mcp-parity.md
CHANGED
|
@@ -23,6 +23,7 @@ This document maps the feature parity and documents the remaining gaps.
|
|
|
23
23
|
| **Session-start reminder** (recall nudge + hygiene heartbeat) | `session.created` event → `client.session.prompt` injects a synthetic message | `SessionStart` hook → `thatch reminder`; stdout becomes context | `sessionStart` hook → `thatch reminder --json`; output is `additional_context` JSON | **Full** |
|
|
24
24
|
| **Compaction context** (re-familiarize after compaction) | `experimental.session.compacting` hook appends context to the compaction output | `PostCompact` hook — side-effects only, **cannot inject context** | No equivalent hook | **Gapped** — see below |
|
|
25
25
|
| **Wrap-up commands** (`/thatch/compact`, `/thatch/exit`) | Command markdown synced to `$XDG_CONFIG_HOME/opencode/command/thatch/` at plugin init; `command.execute.before` arms the session and the next idle resolves the greenlight token into a TUI action (`session.compact` via `executeCommand`, `app.exit` via `publish`) | No equivalent — no plugin host to arm the check or trigger the action | No equivalent | **Gapped** — needs the plugin's SDK client and TUI control routes |
|
|
26
|
+
| **On-demand actions** (`/thatch/defrag`, `extract`, `hygiene`, `reflect`) | Command markdown synced at plugin init; bodies rendered from the shared prompt cores with `thatch_` tool spellings; `extract` is opencode-only (needs session identity via `get_session_info` and the plugin's payload provider) | Command markdown synced by `thatch setup --claude` into `<claudeDir>/commands/thatch/`; same cores with `mcp__thatch__` spellings; `extract` excluded | MCP Prompts (`prompts/list` + `prompts/get`) from the same cores; Cursor surfaces them as slash commands; `extract` excluded | **Full** for the shared actions (same cores, per-host tool spellings); `extract` opencode-only |
|
|
26
27
|
| **Extraction** (buffer tool calls, extract durable facts) | `tool.execute.after` buffers non-thatch, non-skill, non-task tool calls in-process; **direct extraction** is the primary path — when the parent session goes idle with pending buffer entries, the plugin creates a child session via the SDK (`client.session.create` with `parentID`) and prompts it to call `get_extraction_payload` (nudge suppressed via the `extracting` set); the `chat.message` nudge path is a **fallback** used when direct extraction throws | `PostToolBatch` → `thatch buffer-batch` (file-backed JSONL queue); `UserPromptSubmit` → `thatch flush-tools` peeks the queue and prints the nudge with session ID + fetch tool name (does not drain — the queue persists until a memory write or `extraction_done`) | `postToolUse` → `thatch buffer-tool` (single-tool, file-backed queue); `beforeSubmitPrompt` → `thatch flush-tools --json` | **Partial** — opencode uses direct extraction (parent idle → child session) as the primary path, with the nudge as fallback; MCP hosts are nudge-only. Both paths use `get_extraction_payload` to fetch the queued interactions as a tool response, keeping the full payload out of the main session's context window. Nudge timing differs: the nudge arrives at the **start** of the next turn in Claude Code/Cursor; opencode's direct extraction fires at **end of turn** (session idle), and the nudge fallback fires at the start of the next turn like MCP |
|
|
27
28
|
| **Extraction feedback** (toast notifications) | `client.tui.showToast` fires when the extraction child goes idle — shows `[thatch] new: N, updated: M, deleted: K` (success) or `extraction complete — nothing to save` (info); best-effort, silently ignored in headless mode | No TUI connection — MCP hosts have no equivalent | No TUI connection | **Gapped** — MCP hosts cannot show toast notifications |
|
|
28
29
|
| **Prompt-aware recall nudge** | `chat.message` hook embeds the prompt with the in-process warm model, searches `db.search()`, pushes a nudge part if matches ≥ threshold | `UserPromptSubmit` hook → `thatch flush-tools` connects to the MCP server's sideband socket; the warm server embeds + searches; hook prints the nudge or falls back to the write nudge | `beforeSubmitPrompt` hook → `thatch flush-tools --json` (same sideband path) | **Full** — sideband socket gives cold hook processes access to the warm MCP server's model |
|
|
@@ -50,10 +50,12 @@ the TUI is not connected (headless mode).
|
|
|
50
50
|
| Instructions | `CLAUDE.md` (idempotent `appendBlock`) | `$CLAUDE_CONFIG_DIR/CLAUDE.md` |
|
|
51
51
|
| Hooks | `.claude/settings.json` | `$CLAUDE_CONFIG_DIR/settings.json` |
|
|
52
52
|
| Skills | `.claude/skills/` (in the repo) | `$CLAUDE_CONFIG_DIR/skills/` |
|
|
53
|
+
| Commands | `.claude/commands/thatch/` (in the repo) | `$CLAUDE_CONFIG_DIR/commands/thatch/` |
|
|
53
54
|
|
|
54
55
|
`CLAUDE_CONFIG_DIR` overrides the default `~/.claude` for all user-scoped
|
|
55
56
|
paths. Project-local keeps everything in the repo: `.mcp.json`, `CLAUDE.md`,
|
|
56
|
-
`.claude/settings.json`, and `.claude/
|
|
57
|
+
`.claude/settings.json`, `.claude/skills/`, and `.claude/commands/thatch/`
|
|
58
|
+
(so skills and commands version with the
|
|
57
59
|
project and every contributor gets them).
|
|
58
60
|
|
|
59
61
|
### Hook events (nested `settings.json`)
|