@jeffober/thatch 0.1.40 → 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/thatch-code-review.md +4 -4
- package/artifacts/skills/thatch-review-synthesizer.md +8 -8
- package/artifacts/skills/{thatch-review-mentorship.md → thatch-review-technique.md} +9 -10
- package/bin/thatch +160 -16
- package/docs/dev/README.md +26 -14
- package/docs/dev/features/README.md +1 -0
- 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/multi-host.md +17 -9
- 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 +5 -4
- package/docs/user/README.md +23 -5
- package/docs/user/cli.md +30 -0
- package/docs/user/code-review.md +1 -1
- package/docs/user/cross-session-chat.md +102 -0
- package/docs/user/setup.md +16 -2
- package/docs/user/skills.md +7 -6
- package/package.json +1 -1
- package/src/chat-names.ts +121 -0
- package/src/chat.ts +879 -0
- package/src/db.ts +179 -0
- package/src/index.ts +113 -5
- package/src/prompts.ts +133 -8
- package/src/setup.ts +83 -29
- package/src/skills.ts +47 -17
- package/src/tool-defs.ts +346 -0
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Include thatch's instructions in your agent's system prompt manually; see
|
|
|
66
66
|
|
|
67
67
|
## What's inside
|
|
68
68
|
|
|
69
|
-
Thatch gives your agent
|
|
69
|
+
Thatch gives your agent:
|
|
70
70
|
|
|
71
71
|
- **Memory** -- save, search, and recall knowledge across sessions with
|
|
72
72
|
local embeddings (bge-small-en-v1.5) and SQLite. Every project gets its
|
|
@@ -92,8 +92,15 @@ Thatch gives your agent three layers of persistent context:
|
|
|
92
92
|
only). The agent registers a watch on a GitHub PR or branch (main); thatch polls it in the
|
|
93
93
|
background and prompts the session when comments, commits, CI results, or
|
|
94
94
|
status changes arrive. Notifications carry pointer data only.
|
|
95
|
-
- **
|
|
96
|
-
|
|
95
|
+
- **Cross-session chat** -- your agent sessions can message each other
|
|
96
|
+
(opencode, Claude Code, and Cursor; same machine). A session registers
|
|
97
|
+
under a unique name and topic, others see it in the directory, and
|
|
98
|
+
messages land in its inbox -- opencode sessions get woken with a prompt
|
|
99
|
+
when idle, other hosts see pending mail at their next prompt. Loop-safe:
|
|
100
|
+
messages are informational to the receiving agent, and wake prompts are
|
|
101
|
+
rate-capped.
|
|
102
|
+
- **Notifications + user config** -- the agent can ping you out-of-band when a
|
|
103
|
+
long-running outcome lands: a desktop banner, a spoken voice
|
|
97
104
|
announcement, or both (macOS and Linux). Preferences live in a
|
|
98
105
|
hand-editable config file (`~/.config/thatch/config.json`) that the agent
|
|
99
106
|
manages through `config_get`/`config_set` -- ask it to change your voice
|
|
@@ -105,7 +105,7 @@ For each review unit, dispatch sub-agents using the Task tool. Each sub-agent ru
|
|
|
105
105
|
|
|
106
106
|
8. **Highlights** — positive finding detection: notably clever solutions, cleanup done along the way, documentation that meaningfully helps. Dispatch a sub-agent with instructions to: read every changed file in the unit and the before-state with git show, look for things that rise above baseline competence. Medium-high bar: no generic praise, no baseline competence. If nothing rises above the bar, it reports "No highlights" and produces no findings.
|
|
107
107
|
|
|
108
|
-
9. **
|
|
108
|
+
9. **Technique** — teaching-grade lens on better ways to do it: helpers and internal packages the author may not have seen, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Dispatch a sub-agent with instructions to: read every changed file in the unit, verify any cited alternative exists and achieves the same behavior before reporting, frame notes generously, report all notes as informational. If nothing qualifies, it reports "No technique notes" and produces no findings.
|
|
109
109
|
|
|
110
110
|
For the integration review unit (5+ points), dispatch a sub-agent focused on:
|
|
111
111
|
- Cross-component contracts — do the interfaces between components match?
|
|
@@ -137,7 +137,7 @@ Confirmed LOW findings are mandatory in the final report, including mechanical f
|
|
|
137
137
|
|
|
138
138
|
Confirmed highlights (from the highlights specialist) appear in a `### Highlights` section after the workflow changes and before the confirmed findings. Most PRs will have none — that is expected. Do not pad with borderline calls. But when the highlights specialist found genuine standouts, include every one that passes the bar.
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
Technique notes (from the technique specialist) appear in a `### Technique notes` section after Highlights and before the confirmed findings. They are teaching-grade observations (existing helpers, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability), always informational, never merged into defect findings and never blocking. Do not pad: an empty section ("None") is the common case, and a note only appears when the cited alternative was verified to exist and achieve the same behavior.
|
|
141
141
|
|
|
142
142
|
**Follow-up round cross-reference** — if Step 2 built a prior-comments register, the synthesizer must cross-reference every confirmed and rejected finding against it (see the synthesizer skill's "Cross-reference against prior review comments" section). The final report includes a `### Previously identified findings` appendix listing each prior comment with its final status (`addressed`, `still active — reproduced by finding X`, `still active — not reproduced this round, re-verified above`, or `unclear`), the original author, date, and original location. New findings that match an addressed prior comment are still reported in the main findings (with attribution) and warrant a closer look — the prior round's resolution may be incomplete or the issue may have regressed.
|
|
143
143
|
|
|
@@ -145,10 +145,10 @@ Alternatively, perform the synthesis yourself:
|
|
|
145
145
|
1. Read each finding's cited location to verify evidence accuracy.
|
|
146
146
|
2. Deduplicate findings flagged by multiple specialists.
|
|
147
147
|
3. Group findings by root cause where multiple findings stem from the same issue.
|
|
148
|
-
4. Classify each as CONFIRMED, REJECTED, or UNVERIFIABLE. For behavioral findings, apply citation verification, reachability, and intent verification. For mechanical findings, verify the cited text exists, is branch-introduced or newly made relevant, and violates the stated guideline or specialist taxonomy. For highlights, verify the cited text exists, the claim is accurate, and the highlighted thing genuinely rises above baseline competence. For
|
|
148
|
+
4. Classify each as CONFIRMED, REJECTED, or UNVERIFIABLE. For behavioral findings, apply citation verification, reachability, and intent verification. For mechanical findings, verify the cited text exists, is branch-introduced or newly made relevant, and violates the stated guideline or specialist taxonomy. For highlights, verify the cited text exists, the claim is accurate, and the highlighted thing genuinely rises above baseline competence. For technique notes, verify the cited alternative exists at the cited location and achieves the same behavior; if the code as written is actually a defect, hand the finding to the defect track instead. When the brief reports staleness (main moved past the merge-base touching this change's paths), re-verify each behavioral finding against current main first — a finding true only at the merge-base is a rebase note for the author, not a defect in this PR.
|
|
149
149
|
5. Cross-reference against the prior-comments register if one was built in Step 2: tag matching findings `Provenance: previously identified by @author, PR #N, DATE` and produce the `### Previously identified findings` appendix per the synthesizer skill.
|
|
150
150
|
6. Calibrate severity (BLOCKING > HIGH > MEDIUM > LOW) based on your verification.
|
|
151
|
-
7. Produce a final report with a workflow-change preface, highlights (if any),
|
|
151
|
+
7. Produce a final report with a workflow-change preface, highlights (if any), technique notes (if any), findings grouped by severity, coverage gaps, and human-verifiable unknowns noted. Include every confirmed LOW finding.
|
|
152
152
|
8. Apply the synthesizer's "Writing review comments" prose rules to the whole report. Findings are prose, not shorthand: plain English, one idea per sentence, one sentence per hop of any producer chain.
|
|
153
153
|
|
|
154
154
|
## Specialist briefing template
|
|
@@ -3,7 +3,7 @@ name: thatch-review-synthesizer
|
|
|
3
3
|
description: Verify and synthesize findings from multiple review specialist skills into a single deduplicated, severity-grouped report. Use after running one or more thatch-review-* specialist skills.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are a review synthesizer. You have received findings from one or more review specialists (pedantic, acceptance, state-flow, economy, no-slop, breadcrumbs, mark-and-sweep, highlights,
|
|
6
|
+
You are a review synthesizer. You have received findings from one or more review specialists (pedantic, acceptance, state-flow, economy, no-slop, breadcrumbs, mark-and-sweep, highlights, technique). Your job is to verify their citations against the actual code, deduplicate across specialists, and produce a single, coherent final report.
|
|
7
7
|
|
|
8
8
|
## Static analysis only
|
|
9
9
|
You review code by reading it. Do NOT run tests, linters, compilers, or any build commands.
|
|
@@ -48,13 +48,13 @@ For each finding from the specialists:
|
|
|
48
48
|
- For CLEANUP highlights: verify the bad code existed in the before-state (use git show with the base commit). If the "cleaned up" code was actually just new code the author wrote, it does not count.
|
|
49
49
|
Runtime reachability, producer chains, and intent verification do not apply. A highlight is not a bug and does not need a trigger scenario.
|
|
50
50
|
|
|
51
|
-
4i. **
|
|
51
|
+
4i. **Technique verification (technique).** For technique notes, verification means:
|
|
52
52
|
- The cited alternative exists at the cited location (`path:line` read directly; documentation links confirmed, not assumed). A note citing a helper that does not exist is rejected outright.
|
|
53
53
|
- The equivalence claim is accurate: the cited alternative achieves the same behavior as the code under review, including error cases. If the current code relies on a difference the alternative cannot provide, reject.
|
|
54
54
|
- The code as written is not a defect. If it is, the note belongs to the defect track (state-flow, acceptance, economy) — reclassify or hand it back, do not report a defect as a teaching note.
|
|
55
55
|
- The note is teaching-grade, not condescension: the framing is "you may not have seen this", and the author's diff does not already demonstrate they know it.
|
|
56
56
|
- For TEST_CRAFT notes: verify the repo's own test conventions (read sibling test files) before confirming — repo convention beats the lens preference.
|
|
57
|
-
Runtime reachability, producer chains, and intent verification do not apply. A
|
|
57
|
+
Runtime reachability, producer chains, and intent verification do not apply. A technique note is not a bug and does not need a trigger scenario. All technique notes are informational and never blocking.
|
|
58
58
|
|
|
59
59
|
4e. **Economy verification (economy).** For economy findings (unnecessary complexity, simpler alternatives, redundancy, tradeoffs), verification means:
|
|
60
60
|
- The cited code exists at the cited location (already confirmed in step 2).
|
|
@@ -65,8 +65,8 @@ For each finding from the specialists:
|
|
|
65
65
|
Runtime reachability, producer chains, and intent verification do not apply. An economy finding is a design observation, not a user-triggered bug.
|
|
66
66
|
|
|
67
67
|
7. **Classify:**
|
|
68
|
-
- **CONFIRMED**: For behavioral findings: the cited code matches, the claim is accurate, the bug is reachable through realistic usage, you proved the workflow/producer chain where applicable, the behavior is not intentional, and for data-shape findings you read and cited the governing constraint confirming the claimed state is reachable. For mechanical findings: the cited text exists, is branch-introduced or newly made relevant, and violates the stated guideline or specialist taxonomy. For economy findings (OVERENGINEERED, SIMPLER_AVAILABLE, REDUNDANT): the simpler alternative is concrete, achieves the same behavior, no constraint justifies the current complexity, and the complexity is branch-introduced. For economy TRADEOFF findings: both sides of the tradeoff are real (the abstraction removes duplication AND creates a cross-module dependency), and the finding is framed as a question, not a verdict. For highlights: the cited text exists, the claim is accurate, and the highlighted thing genuinely rises above baseline competence. For
|
|
69
|
-
- **REJECTED**: For behavioral findings: the citation is wrong, the claim is inaccurate, the bug cannot manifest in the actual runtime context, the behavior is an intentional design decision (explain why briefly), or a governing constraint (NOT NULL, FK, type, guard) forecloses the claimed state. Reject findings that rely on manually seeded invalid state/data with no real producer path. For mechanical findings: the cited text does not exist, does not violate the stated guideline, is unchanged legacy outside the touched scope, or the finding duplicates another confirmed finding. For economy findings: a constraint justifies the complexity (cite it), the simpler alternative does not achieve the same behavior, or the complexity predates the change. For economy TRADEOFF findings: only one side of the tradeoff is real (e.g., the abstraction removes duplication but the consumers are in the same package with no cross-module dependency), or the specialist declared a verdict instead of presenting the tradeoff. For highlights: the cited text does not exist, the claim is inaccurate, or the highlight does not meet the bar (generic praise, baseline competence, a compliment that could apply to any PR). For
|
|
68
|
+
- **CONFIRMED**: For behavioral findings: the cited code matches, the claim is accurate, the bug is reachable through realistic usage, you proved the workflow/producer chain where applicable, the behavior is not intentional, and for data-shape findings you read and cited the governing constraint confirming the claimed state is reachable. For mechanical findings: the cited text exists, is branch-introduced or newly made relevant, and violates the stated guideline or specialist taxonomy. For economy findings (OVERENGINEERED, SIMPLER_AVAILABLE, REDUNDANT): the simpler alternative is concrete, achieves the same behavior, no constraint justifies the current complexity, and the complexity is branch-introduced. For economy TRADEOFF findings: both sides of the tradeoff are real (the abstraction removes duplication AND creates a cross-module dependency), and the finding is framed as a question, not a verdict. For highlights: the cited text exists, the claim is accurate, and the highlighted thing genuinely rises above baseline competence. For technique notes: the cited alternative exists, achieves the same behavior, the code as written is not a defect, and the note is teaching-grade rather than condescension.
|
|
69
|
+
- **REJECTED**: For behavioral findings: the citation is wrong, the claim is inaccurate, the bug cannot manifest in the actual runtime context, the behavior is an intentional design decision (explain why briefly), or a governing constraint (NOT NULL, FK, type, guard) forecloses the claimed state. Reject findings that rely on manually seeded invalid state/data with no real producer path. For mechanical findings: the cited text does not exist, does not violate the stated guideline, is unchanged legacy outside the touched scope, or the finding duplicates another confirmed finding. For economy findings: a constraint justifies the complexity (cite it), the simpler alternative does not achieve the same behavior, or the complexity predates the change. For economy TRADEOFF findings: only one side of the tradeoff is real (e.g., the abstraction removes duplication but the consumers are in the same package with no cross-module dependency), or the specialist declared a verdict instead of presenting the tradeoff. For highlights: the cited text does not exist, the claim is inaccurate, or the highlight does not meet the bar (generic praise, baseline competence, a compliment that could apply to any PR). For technique notes: the cited alternative does not exist, does not achieve the same behavior, the code as written is a defect (reclassify onto the defect track instead), the author's diff already demonstrates the knowledge, or the note cannot survive the "you may not have seen this" register.
|
|
70
70
|
- **UNVERIFIABLE**: The citation is correct but you cannot confirm the claim without deeper tracing. This is the default for plausible behavioral claims that lack a proven trigger path, producer chain, or authoritative source of truth. A data-shape finding that lacks the governing-constraint citation is UNVERIFIABLE, not CONFIRMED. Mechanical findings should rarely be UNVERIFIABLE — if the text exists and violates the guideline, it is CONFIRMED. Economy findings should rarely be UNVERIFIABLE — either the simpler alternative achieves the same behavior and no constraint justifies the complexity (CONFIRMED), or it does not (REJECTED). Highlights should rarely be UNVERIFIABLE — either the code does what the highlight claims and it meets the bar (CONFIRMED), or it does not (REJECTED).
|
|
71
71
|
|
|
72
72
|
## Deduplication
|
|
@@ -115,7 +115,7 @@ Confirmed LOW findings are mandatory. Do not summarize them away or omit them fo
|
|
|
115
115
|
|
|
116
116
|
Confirmed highlights are optional in the sense that most PRs will have none. Do not pad the section with borderline calls. An empty highlights section ("None") is honest and expected for competent but unremarkable code. But when the highlights specialist did find genuine standouts, include every one that passes the bar.
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
Technique notes follow the same rule: most PRs will have none, an empty section ("None") is the expected outcome, and every included note must have passed verification. They are informational and never merged into the defect findings.
|
|
119
119
|
|
|
120
120
|
The report is user-facing. Start by teaching the changed workflows before listing findings. Use the workflow guide and project context supplied by the coordinator. This is not a second PR description; it is the reviewer's map of what changed so the findings have a place to land.
|
|
121
121
|
|
|
@@ -273,13 +273,13 @@ For each highlight:
|
|
|
273
273
|
5. **Evidence**: the code you read to confirm it (quote the exact lines you verified)
|
|
274
274
|
6. **Why it stands out**: what makes this above the bar — be specific
|
|
275
275
|
|
|
276
|
-
###
|
|
276
|
+
### Technique notes
|
|
277
277
|
|
|
278
278
|
Teaching-grade observations for the author: helpers and internal packages they may not have seen, companion techniques, house patterns, test-craft, API-shape principles, next-reader discoverability. Only include notes that passed verification — the cited alternative exists, achieves the same behavior, and the code as written is not a defect. These are informational and never blocking. If none, write "None."
|
|
279
279
|
|
|
280
280
|
For each note:
|
|
281
281
|
1. **Category** (AWARENESS, PAIRED_TECHNIQUE, INTERNAL_PATTERN, IDIOM, TEST_CRAFT, MISUSE_PROOFING, NEXT_READER, API_SHAPE)
|
|
282
|
-
2. **Source**:
|
|
282
|
+
2. **Source**: technique specialist
|
|
283
283
|
3. **Location**: file:line
|
|
284
284
|
4. **Observation**: what the code does now, stated neutrally
|
|
285
285
|
5. **The easier path**: the named alternative with its citation
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: thatch-review-
|
|
3
|
-
description: '
|
|
2
|
+
name: thatch-review-technique
|
|
3
|
+
description: 'Technique review lens — teaching-grade observations the author can act on or dismiss: helpers and internal packages they may not have seen, companion techniques, house patterns, test-craft, API-shape principles, and discoverability for the next reader. Informational only, never blocking, empty is honest. Use for post-implementation review of a branch, PR, or commit range.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
You are a
|
|
6
|
+
You are a technique reviewer. Your job is to find places where the author would benefit from knowing something the codebase, the language, or the platform already knows: a helper that exists, a house pattern that is battle-tested, a test-design habit that avoids brittle tests, an API-shape principle that keeps callers free. You are not looking for defects — other specialists own defects. You are looking for teaching moments: the code can be correct and still be the second-best way to do it.
|
|
7
7
|
|
|
8
8
|
Code review is a form of peer mentorship. A review that only lists bugs transfers nothing; a review that also transfers knowledge makes every future change by this author cheaper. The two guiding principles of this lens: make the right thing the easiest thing to do, and where possible make the wrong thing impossible instead of unlikely.
|
|
9
9
|
|
|
@@ -28,7 +28,7 @@ Three conditions, all required:
|
|
|
28
28
|
2. **The code as written is not wrong.** If the issue is a defect, it belongs to the defect-finding specialists (state-flow, acceptance, economy), not here. Boundary with economy: economy reports defect-grade redundancy (reimplementation without justification, complexity that fails its bar); you report teaching-grade awareness (the code is fine, the path is easier elsewhere). When in doubt about whether something is a defect, leave it to economy and do not report it.
|
|
29
29
|
3. **The author plausibly lacks the knowledge.** Do not re-teach what the diff already demonstrates: if the author clearly applies the idiom elsewhere in the same change, they know it, and one deviation is a choice, not a gap.
|
|
30
30
|
|
|
31
|
-
If nothing qualifies, report: "No
|
|
31
|
+
If nothing qualifies, report: "No technique notes." Do not manufacture teaching moments. A padded technique section reads as condescension and trains authors to skim.
|
|
32
32
|
|
|
33
33
|
## Categories
|
|
34
34
|
|
|
@@ -47,8 +47,7 @@ What does NOT count:
|
|
|
47
47
|
Evidence: cite the helper at `path:line` (internal) or a documentation link (external). Verify it achieves the same behavior, including error cases, before reporting.
|
|
48
48
|
|
|
49
49
|
### PAIRED_TECHNIQUE
|
|
50
|
-
The technique in the diff has a well-known companion that materially improves it, and the diff does not include the companion.
|
|
51
|
-
|
|
50
|
+
The technique in the diff has a well-known companion (a better idiom, tool, or API) that materially improves it, and the diff does not include the companion.
|
|
52
51
|
What counts:
|
|
53
52
|
- Unbounded fan-out (one goroutine/task per item, no concurrency cap) where the singleflight pattern or a bounded worker pool is the standard companion once load arrives
|
|
54
53
|
- Retry or backoff without jitter in code that will run under contention
|
|
@@ -146,7 +145,7 @@ What does NOT count:
|
|
|
146
145
|
- Anything that is a defect; hand it to the defect specialists instead
|
|
147
146
|
- Style preferences with no teaching content
|
|
148
147
|
- Teaching the author something their own diff already demonstrates they know
|
|
149
|
-
- Condescension in any form: the register is "you may not have seen this", and if a note cannot survive that register, it is not a
|
|
148
|
+
- Condescension in any form: the register is "you may not have seen this", and if a note cannot survive that register, it is not a technique note
|
|
150
149
|
|
|
151
150
|
## Method
|
|
152
151
|
|
|
@@ -161,7 +160,7 @@ Do NOT report on files you did not actually read.
|
|
|
161
160
|
|
|
162
161
|
## Output format
|
|
163
162
|
|
|
164
|
-
Produce
|
|
163
|
+
Produce technique notes as markdown. For each note:
|
|
165
164
|
|
|
166
165
|
### [CATEGORY] — file:line
|
|
167
166
|
- **Observation**: what the code does now, stated neutrally
|
|
@@ -169,6 +168,6 @@ Produce mentorship notes as markdown. For each note:
|
|
|
169
168
|
- **Why it matters**: the concrete benefit (less code to own, a bug class foreclosed, the next dev finds it faster) in one or two sentences
|
|
170
169
|
- Optionally close with a probe: "Would `<alternative>` work here, or is there a reason to rule it out?" A probe is a genuine question with a pre-offered answer — use it when the constraint, if any, is genuinely invisible to you; do not use it to soften a claim you can verify yourself.
|
|
171
170
|
|
|
172
|
-
All
|
|
171
|
+
All technique notes are informational. They ride along with any review verdict and never block a merge.
|
|
173
172
|
|
|
174
|
-
If there are no notes, say so explicitly: "No
|
|
173
|
+
If there are no notes, say so explicitly: "No technique notes." An empty section is the correct output when the code is competent and nothing rose to teaching level, which is the common case.
|
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) |
|