@jeffober/thatch 0.1.34 → 0.1.36
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 +15 -0
- package/artifacts/skills/thatch-code-review.md +8 -2
- package/artifacts/skills/thatch-review-context.md +15 -3
- package/artifacts/skills/thatch-review-synthesizer.md +7 -5
- package/bin/thatch +157 -0
- package/docs/dev/README.md +10 -3
- package/docs/dev/features/README.md +1 -0
- package/docs/dev/features/multi-host.md +16 -10
- package/docs/dev/features/watchers.md +150 -0
- package/docs/dev/mcp-parity.md +1 -1
- package/docs/dev/skills.md +7 -5
- package/docs/user/README.md +25 -1
- package/docs/user/code-review.md +11 -3
- package/docs/user/skills.md +1 -1
- package/docs/user/watchers.md +79 -0
- package/package.json +1 -1
- package/src/index.ts +61 -2
- package/src/mcp.ts +4 -1
- package/src/prompts.ts +43 -7
- package/src/session-db.ts +452 -0
- package/src/skills.ts +22 -46
- package/src/tool-defs.ts +269 -3
- package/src/tools.ts +11 -3
- package/src/watchers.ts +511 -0
package/README.md
CHANGED
|
@@ -83,6 +83,15 @@ Thatch gives your agent three layers of persistent context:
|
|
|
83
83
|
situations arise. The agent ham/spams each surfaced rule to train the
|
|
84
84
|
classifier. Confidence adjusts the same way as predictions. An anti-laziness
|
|
85
85
|
guard in the prompt prevents the agent from codifying shortcuts.
|
|
86
|
+
- **Conversation search** -- the agent can search its own past opencode
|
|
87
|
+
conversations by substring or regex and retrieve full messages, including
|
|
88
|
+
complete tool inputs and outputs. Available as tools (`session_search`,
|
|
89
|
+
`session_get`, opencode only) and as CLI subcommands (`thatch session
|
|
90
|
+
list/get/transcript/search`, JSONL output designed for piping to `jq`).
|
|
91
|
+
- **Watchers** -- event-driven notifications from external sources (opencode
|
|
92
|
+
only). The agent registers a watch on a GitHub PR; thatch polls it in the
|
|
93
|
+
background and prompts the session when comments, commits, CI results, or
|
|
94
|
+
status changes arrive. Notifications carry pointer data only.
|
|
86
95
|
|
|
87
96
|
Plus **26 skills** for memory workflows, structured multi-specialist code
|
|
88
97
|
review, review response, change and feature walkthroughs, memory verification,
|
|
@@ -98,6 +107,12 @@ host.
|
|
|
98
107
|
Everything is local. The embedding model downloads once from Hugging Face Hub
|
|
99
108
|
and is cached. No data leaves your machine.
|
|
100
109
|
|
|
110
|
+
One exception: watchers (opencode only) call the GitHub API through the `gh`
|
|
111
|
+
CLI to poll PRs you explicitly asked to watch, using your existing gh
|
|
112
|
+
authentication. Thatch never sees or stores the token, and notifications
|
|
113
|
+
carry pointer data only - GitHub comment text never enters your context
|
|
114
|
+
unless the agent fetches it.
|
|
115
|
+
|
|
101
116
|
## Development
|
|
102
117
|
|
|
103
118
|
```bash
|
|
@@ -26,6 +26,8 @@ Also detect whether the current branch has a **connected PR/MR** even when none
|
|
|
26
26
|
|
|
27
27
|
Also fetch any refs not locally reachable so branches and PRs that were never checked out can be reviewed (e.g. `git fetch origin <branch>` for a PR head).
|
|
28
28
|
|
|
29
|
+
**Anchor discipline**: the working tree may be checked out on any branch — main, another feature, a detached HEAD — and is not guaranteed to match the change under review. Resolve the head SHA of the review range (`git rev-parse <head-ref>`), and compute every file:line citation and review-comment anchor against that ref with `git show <head-sha>:<path>` — never from the working tree, and never from the base. When the working tree happens to be the PR branch this is equivalent, so apply the rule unconditionally rather than branching on it. Pass the head SHA to sub-agents (see the briefing template).
|
|
30
|
+
|
|
29
31
|
Run git diff --stat on the resolved range and git log --oneline to understand the change.
|
|
30
32
|
|
|
31
33
|
## Step 2: Gather project context
|
|
@@ -38,7 +40,9 @@ Load the thatch-review-context skill and follow its methodology to build a conte
|
|
|
38
40
|
- thatch_memory_recall for feature/project status, design decisions, ticket dependencies
|
|
39
41
|
- Project docs, READMEs, design docs
|
|
40
42
|
- Issue tracker (gh issue/view) if ticket IDs are found
|
|
41
|
-
- **
|
|
43
|
+
- **Links in the change** — docs, designs, tickets, and related PRs linked from the PR description, commit messages, or the changed files themselves; follow them for scope and design context (see source #8 in the review-context skill)
|
|
44
|
+
- **Staleness check** — whether main has moved past the merge-base touching the same paths (see source #3 in the review-context skill); when it has, record the commits in the brief
|
|
45
|
+
- **Prior review comments** — if Step 1 detected a connected PR/MR, fetch ALL prior review comments on it (see source #9 in the review-context skill) and build the prior-comments register with a preliminary addressed-check status per comment. This is what distinguishes a **follow-up round** (register non-empty) from the **first round** (register empty or PR has no prior reviews). If Step 1 found no connected PR/MR, the brief explicitly states `Local-branch review — no prior-comment fetch`.
|
|
42
46
|
|
|
43
47
|
Produce the context brief in the format the skill prescribes (project context, this change's scope, deferred work, dependencies, relevant constraints, and the prior review comments section when present).
|
|
44
48
|
|
|
@@ -123,6 +127,7 @@ Pass the synthesizer:
|
|
|
123
127
|
- The workflow guide from Step 3.
|
|
124
128
|
- The specialist findings.
|
|
125
129
|
- The prior-comments register when present.
|
|
130
|
+
- The staleness signal from the brief (commits on main after the merge-base touching this change's paths), when present.
|
|
126
131
|
|
|
127
132
|
The final user-facing report MUST begin with a concise explanation of the workflow(s) this PR changes before listing findings. Use one `### Workflow changes` section, with one subheading per affected workflow when there are multiple. Use the before/now walkthrough shape from the thatch-pr-description skill: numbered step = existing behavior; `NOW` sub-bullet = what the PR changes; `N/A` for new stages. This gives the user a map of the change before the findings. Keep it short: 3-6 steps per workflow.
|
|
128
133
|
|
|
@@ -136,7 +141,7 @@ Alternatively, perform the synthesis yourself:
|
|
|
136
141
|
1. Read each finding's cited location to verify evidence accuracy.
|
|
137
142
|
2. Deduplicate findings flagged by multiple specialists.
|
|
138
143
|
3. Group findings by root cause where multiple findings stem from the same issue.
|
|
139
|
-
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.
|
|
144
|
+
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. 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.
|
|
140
145
|
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.
|
|
141
146
|
6. Calibrate severity (BLOCKING > HIGH > MEDIUM > LOW) based on your verification.
|
|
142
147
|
7. Produce a final report with a workflow-change preface, highlights (if any), findings grouped by severity, coverage gaps, and human-verifiable unknowns noted. Include every confirmed LOW finding.
|
|
@@ -145,6 +150,7 @@ Alternatively, perform the synthesis yourself:
|
|
|
145
150
|
|
|
146
151
|
When dispatching each sub-agent, include in the prompt:
|
|
147
152
|
- The git range to review
|
|
153
|
+
- **The resolved head SHA of the range** — all file reads, file:line citations, and anchors MUST be computed against this ref (`git show <head-sha>:<path>`), never the working tree, which may be on a different branch than the change under review
|
|
148
154
|
- The specific files in this unit's scope
|
|
149
155
|
- The specialist focus (from the eight specialists above)
|
|
150
156
|
- The diff stat for this unit's files
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: thatch-review-context
|
|
3
|
-
description: Gather project and feature context before a code review. Investigates PR descriptions, git history, ticket references, docs, and memory to build a context brief that prevents false positives about intentionally deferred work. Use before dispatching review specialists or running a solo review.
|
|
3
|
+
description: Gather project and feature context before a code review. Investigates PR descriptions, git history, ticket references, linked docs/tickets, docs, and memory to build a context brief that prevents false positives about intentionally deferred work. Use before dispatching review specialists or running a solo review.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
You are a review context builder. Your job is to gather project context that gives reviewers the background they need to avoid false positives about intentionally deferred work, incomplete features, and multi-ticket dependencies.
|
|
@@ -28,6 +28,7 @@ Branch names often contain ticket identifiers. Look for patterns:
|
|
|
28
28
|
- `git log --all --oneline --grep="TICKET-ID"` — find related commits on other branches
|
|
29
29
|
- `git log -S "TODO (TICKET"` — find TODO markers that reference this or related tickets
|
|
30
30
|
- `git branch -a` — other branches may show planned or in-progress related work
|
|
31
|
+
- **Staleness check**: `git log --oneline <merge-base>..origin/main -- <changed paths>` — commits that landed on main AFTER this branch's merge-base, touching the same files. If any exist, the branch is stale relative to main and findings true at the merge-base may already be moot. List these commits in the brief under Dependencies so the synthesizer re-verifies findings against current main.
|
|
31
32
|
|
|
32
33
|
### 4. TODO ($ticket) markers in the diff
|
|
33
34
|
Scan the diff for TODO markers that reference ticket identifiers:
|
|
@@ -56,7 +57,17 @@ If ticket IDs were found:
|
|
|
56
57
|
- Look for milestone assignments, dependencies, and blocking relationships
|
|
57
58
|
- Check for epic or parent issues that describe the overall feature
|
|
58
59
|
|
|
59
|
-
### 8.
|
|
60
|
+
### 8. Links in the change (follow the references)
|
|
61
|
+
|
|
62
|
+
Collect links to material outside the diff that a reviewer would need for context, then follow them:
|
|
63
|
+
- PR description links: docs, design docs, RFCs, tickets, related PRs
|
|
64
|
+
- Commit message links and ticket references
|
|
65
|
+
- Links inside the changed files themselves: READMEs, doc comments, config comments referencing designs or runbooks
|
|
66
|
+
- Ticket bodies from source 7, which often link parent epics, blocking issues, or design docs
|
|
67
|
+
|
|
68
|
+
Follow each link one hop and read it for review-relevant context: scope, design decisions, constraints, deferred work, sibling changes. If a linked document references another artifact that is directly about this change (a parent epic, a design doc for the same feature), follow that too. Stop at depth 2, and stop following a path once the linked material stops changing what a reviewer would flag. Record anything that changes review context in the brief. A link that 404s or requires access you do not have is not a finding — note it as `link unreachable` and move on.
|
|
69
|
+
|
|
70
|
+
### 9. Prior review comments (follow-up round detection)
|
|
60
71
|
|
|
61
72
|
If the change under review has a connected PR/MR on the upstream remote, fetch all prior review comments so this review does not duplicate already-identified issues. Any prior review activity on the PR/MR means **this is a follow-up round**, not the first review. If no PR/MR is connected, this is a **local-branch review** — skip this source entirely; the existing review procedure applies.
|
|
62
73
|
|
|
@@ -113,7 +124,7 @@ For each comment, attempt to determine whether it has been addressed in the **cu
|
|
|
113
124
|
|
|
114
125
|
The register produced here is a **preliminary** classification. The synthesizer produces the final cross-reference, including any prelim-status overrides when new findings from the specialist round reproduce or refute a prior comment.
|
|
115
126
|
|
|
116
|
-
###
|
|
127
|
+
### 10. Repo structure conventions (implicit)
|
|
117
128
|
Directory structure conventions are often undocumented. They live in the repo's layout itself, not in a CONTRIBUTING.md. Look at the directory tree to identify how the repo organizes code:
|
|
118
129
|
- `git ls-files | head -50` or `ls` at the top level to see the organizational pattern
|
|
119
130
|
- For multi-app repos, identify whether shared packages live inside app directories or at the repo root
|
|
@@ -143,6 +154,7 @@ List each deferred piece:
|
|
|
143
154
|
- What other tickets or PRs this depends on (must merge first)
|
|
144
155
|
- What tickets or PRs depend on this (are blocked by it)
|
|
145
156
|
- Whether base branches have landed (for stacked PRs)
|
|
157
|
+
- **Staleness**: whether main has moved past the merge-base since the branch point. When the staleness check (source 3) found commits touching this change's paths, list them here — the synthesizer re-verifies behavioral findings against current main before classifying them
|
|
146
158
|
|
|
147
159
|
### Relevant constraints
|
|
148
160
|
- Design decisions or architectural constraints from memories or docs
|
|
@@ -31,11 +31,9 @@ For each finding from the specialists:
|
|
|
31
31
|
|
|
32
32
|
5a. **Verify governing constraints for data-shape findings.** For findings claiming a field can be NULL/missing/orphaned/malformed, locate and read the field's definition (schema migration, model, type declaration). If a NOT NULL, FK, enum, or type constraint forecloses the claimed state, REJECT and cite the constraint. Do not classify such a finding CONFIRMED without having read the governing definition.
|
|
33
33
|
|
|
34
|
+
5b. **Re-verify against post-branch-point main (behavioral findings, only when the context brief flags staleness).** If the brief reports commits on main after this branch's merge-base touching this change's paths, the branch may be stale and a finding can be true at the merge-base but already moot on main. For each behavioral finding, read the equivalent code on current main (`git show origin/main:<path>`). A finding that main has already fixed or mooted is a **rebase note**, not a defect in this PR: keep it out of the confirmed findings and list it in the report's rebase-notes appendix instead.
|
|
35
|
+
|
|
34
36
|
6. **Verify intent (behavioral findings only).** If the specialist flagged behavior as a bug but the code appears to work as designed, check whether the behavior is intentional:
|
|
35
|
-
- Read the callers of the cited code to see if the pattern makes sense in context.
|
|
36
|
-
- Check git log or git blame on the cited file for commit messages explaining the decision.
|
|
37
|
-
- Use thatch_memory_recall to search for documented rationale.
|
|
38
|
-
If the behavior is intentional or an accepted limitation, reject the finding.
|
|
39
37
|
|
|
40
38
|
4m. **Mechanical verification (pedantic, no-slop, breadcrumbs, docs, naming, style, comments, structural conventions).** For mechanical findings, verification means:
|
|
41
39
|
- The cited text exists at the cited location (already confirmed in step 2). For PLACEMENT findings, the cited evidence is the file path and the existing directory structure, not quoted code. Verify the file exists at the claimed path and that the existing structure shows the convention the specialist cited.
|
|
@@ -71,7 +69,7 @@ Multiple findings may stem from the same underlying issue (e.g., a contract mism
|
|
|
71
69
|
|
|
72
70
|
## Cross-reference against prior review comments
|
|
73
71
|
|
|
74
|
-
If your input includes a **prior-comments register** (built by the coordinator via `thatch-review-context` source #
|
|
72
|
+
If your input includes a **prior-comments register** (built by the coordinator via `thatch-review-context` source #9 — prior review comments retrieved from the PR/MR's review history), cross-reference every confirmed and rejected finding against it so the final report does not present already-identified issues as fresh discoveries.
|
|
75
73
|
|
|
76
74
|
For each new finding:
|
|
77
75
|
1. **Search for a matching prior comment** by location and semantic claim. Match if both hold:
|
|
@@ -255,6 +253,10 @@ Findings you rejected and a one-line reason why. Include the specialist and loca
|
|
|
255
253
|
|
|
256
254
|
Findings you verified as real but pre-existing, with a one-line note on the issue and its potential impact.
|
|
257
255
|
|
|
256
|
+
### Rebase notes (appendix, brief — only when the context brief flags staleness)
|
|
257
|
+
|
|
258
|
+
Findings that were true at the merge-base but are already fixed or mooted by later commits on main. One line each: the finding, and the main commit that moots it. Omit this section entirely when the brief reports no staleness.
|
|
259
|
+
|
|
258
260
|
### Previously identified findings (appendix — only when a prior-comments register was provided)
|
|
259
261
|
|
|
260
262
|
List every entry from the prior-comments register here. For each entry, produce a one-block record:
|
package/bin/thatch
CHANGED
|
@@ -9,6 +9,7 @@ import { BgeEmbeddingModel } from "../src/embeddings";
|
|
|
9
9
|
import { hygieneReport } from "../src/hygiene";
|
|
10
10
|
import { claudeSessionStartReminder } from "../src/prompts";
|
|
11
11
|
import { setupClaudeCode, setupCursor } from "../src/setup";
|
|
12
|
+
import { resolveOpencodeDbPath, SessionDB, partToTimelineEntry, partToTranscriptEntries, partToFullJson, messageToFullJson, type SessionPart } from "../src/session-db";
|
|
12
13
|
|
|
13
14
|
const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
|
|
14
15
|
const dbPath = process.env.THATCH_DB_PATH ?? join(configHome, "thatch", "thatch.db");
|
|
@@ -34,6 +35,15 @@ function usage(): never {
|
|
|
34
35
|
thatch flush-predictions [--json] Query predictions for the prompt (hook only)
|
|
35
36
|
thatch setup --claude [--cursor] [--global]
|
|
36
37
|
Install config + instructions + hooks + skills
|
|
38
|
+
thatch session list -s <id> [--after T] [--before T]
|
|
39
|
+
JSONL timeline of a session's parts (one line per part)
|
|
40
|
+
thatch session get --id <part_id|msg_id>
|
|
41
|
+
Full JSON of one part (tool parts include their output)
|
|
42
|
+
or one message with all its parts
|
|
43
|
+
thatch session transcript -s <id>
|
|
44
|
+
Full conversation as OpenAI chat-completions JSONL
|
|
45
|
+
thatch session search <query> [--session <id>] [--regex] [--limit N]
|
|
46
|
+
JSONL of matching parts across sessions (decoded content)
|
|
37
47
|
|
|
38
48
|
Stores default to the repo detected from git remote. Use "global" for the
|
|
39
49
|
global store. Use "all" with search to search project + global together.`);
|
|
@@ -67,6 +77,148 @@ function projectDir(): string {
|
|
|
67
77
|
return process.env.CURSOR_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// session subcommand group - read-only archaeology on the opencode db
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
/** Parses an ISO timestamp or epoch-milliseconds value into epoch ms. */
|
|
85
|
+
function parseTimeBound(value: string, flag: string): number {
|
|
86
|
+
const asNum = Number(value);
|
|
87
|
+
if (Number.isFinite(asNum) && value !== "") return asNum;
|
|
88
|
+
const parsed = Date.parse(value);
|
|
89
|
+
if (Number.isNaN(parsed)) {
|
|
90
|
+
console.error(`error: ${flag} expects an ISO timestamp or epoch ms, got "${value}"`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Reads a flag's value (-flag value or --flag=value forms). */
|
|
97
|
+
function flagValue(flags: string[], ...names: string[]): string | undefined {
|
|
98
|
+
for (let i = 0; i < flags.length; i++) {
|
|
99
|
+
for (const name of names) {
|
|
100
|
+
if (flags[i] === name) return flags[i + 1];
|
|
101
|
+
if (flags[i]?.startsWith(`${name}=`)) return flags[i]!.slice(name.length + 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function openSessionDb(): SessionDB {
|
|
108
|
+
const path = resolveOpencodeDbPath();
|
|
109
|
+
if (!path) {
|
|
110
|
+
console.error(
|
|
111
|
+
"error: no opencode database found. The session commands inspect the\n" +
|
|
112
|
+
"session history of the opencode host; other hosts (Claude Code, Cursor)\n" +
|
|
113
|
+
"have no such database. Set OPENCODE_DB to point at an opencode.db file.",
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
return new SessionDB(path);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function sessionCommand(rest: string[]): Promise<void> {
|
|
121
|
+
const [sub, ...restArgs] = rest;
|
|
122
|
+
const sessionID = flagValue(restArgs, "-s", "--session");
|
|
123
|
+
const after = flagValue(restArgs, "--after");
|
|
124
|
+
const before = flagValue(restArgs, "--before");
|
|
125
|
+
|
|
126
|
+
const db = openSessionDb();
|
|
127
|
+
try {
|
|
128
|
+
switch (sub) {
|
|
129
|
+
case "list": {
|
|
130
|
+
if (!sessionID) {
|
|
131
|
+
console.error("error: session list requires -s/--session <session id>");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
const parts = db.listParts(
|
|
135
|
+
sessionID,
|
|
136
|
+
after ? parseTimeBound(after, "--after") : undefined,
|
|
137
|
+
before ? parseTimeBound(before, "--before") : undefined,
|
|
138
|
+
);
|
|
139
|
+
for (const part of parts) console.log(JSON.stringify(partToTimelineEntry(part)));
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
case "get": {
|
|
144
|
+
const id = flagValue(restArgs, "--id");
|
|
145
|
+
if (!id) {
|
|
146
|
+
console.error("error: session get requires --id <part_id|msg_id>");
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
if (id.startsWith("msg_")) {
|
|
150
|
+
const message = db.getMessage(id);
|
|
151
|
+
if (!message) {
|
|
152
|
+
console.error(`error: no message "${id}" in ${db.path}`);
|
|
153
|
+
process.exit(1);
|
|
154
|
+
}
|
|
155
|
+
console.log(JSON.stringify(messageToFullJson(message), null, 2));
|
|
156
|
+
} else {
|
|
157
|
+
const part = db.getPart(id);
|
|
158
|
+
if (!part) {
|
|
159
|
+
console.error(`error: no part "${id}" in ${db.path}`);
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
console.log(JSON.stringify(partToFullJson(part), null, 2));
|
|
163
|
+
}
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
case "transcript": {
|
|
168
|
+
if (!sessionID) {
|
|
169
|
+
console.error("error: session transcript requires -s/--session <session id>");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
for (const message of db.listMessages(sessionID)) {
|
|
173
|
+
for (const entry of partToTranscriptEntries(message.role, message.parts)) {
|
|
174
|
+
console.log(JSON.stringify(entry));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case "search": {
|
|
181
|
+
// Positional = args not preceded by a value-taking flag. Values like
|
|
182
|
+
// "50" or a session id don't start with "-", so predecessor checks
|
|
183
|
+
// are the only reliable discriminator.
|
|
184
|
+
const valueFlags = new Set(["-s", "--session", "--limit"]);
|
|
185
|
+
const positional = restArgs.filter((a, i) => !a.startsWith("-") && !valueFlags.has(restArgs[i - 1] ?? ""));
|
|
186
|
+
const query = positional[0];
|
|
187
|
+
if (!query) {
|
|
188
|
+
console.error("error: session search requires a query (substring, or regex with --regex)");
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
const limit = flagValue(restArgs, "--limit");
|
|
192
|
+
const matches = db.search(query, {
|
|
193
|
+
regex: restArgs.includes("--regex"),
|
|
194
|
+
sessionID,
|
|
195
|
+
limit: limit ? parseInt(limit, 10) : undefined,
|
|
196
|
+
});
|
|
197
|
+
// Titles let a cross-session result list be triaged without a
|
|
198
|
+
// second lookup per hit. Sessions are cached - matches cluster.
|
|
199
|
+
const titles = new Map<string, string | null>();
|
|
200
|
+
for (const part of matches) {
|
|
201
|
+
if (!titles.has(part.sessionId)) {
|
|
202
|
+
titles.set(part.sessionId, db.getSession(part.sessionId)?.title ?? null);
|
|
203
|
+
}
|
|
204
|
+
const entry: Record<string, unknown> = {
|
|
205
|
+
session_id: part.sessionId,
|
|
206
|
+
session_title: titles.get(part.sessionId),
|
|
207
|
+
...partToTimelineEntry(part),
|
|
208
|
+
};
|
|
209
|
+
console.log(JSON.stringify(entry));
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
default:
|
|
215
|
+
usage();
|
|
216
|
+
}
|
|
217
|
+
} finally {
|
|
218
|
+
db.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
70
222
|
switch (command) {
|
|
71
223
|
case "--version":
|
|
72
224
|
case "-v":
|
|
@@ -290,6 +442,11 @@ switch (command) {
|
|
|
290
442
|
break;
|
|
291
443
|
}
|
|
292
444
|
|
|
445
|
+
case "session": {
|
|
446
|
+
await sessionCommand(args);
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
|
|
293
450
|
case "buffer-batch": {
|
|
294
451
|
// PostToolBatch hook: append the batch's tool_calls to the session's
|
|
295
452
|
// JSONL queue. Silent on success - no stdout - so the next model call
|
package/docs/dev/README.md
CHANGED
|
@@ -31,12 +31,17 @@ Shared core
|
|
|
31
31
|
├── prediction.ts → prediction engine wrapper (thin layer over ScoringEngine)
|
|
32
32
|
├── behavior.ts → behavior engine wrapper (thin layer over ScoringEngine)
|
|
33
33
|
├── seed-behaviors.ts → default behavior seeding on first run
|
|
34
|
+
├── session-db.ts → read-only opencode.db access: session timeline, full parts,
|
|
35
|
+
│ decoded-content search, OpenAI transcript builder (backs the
|
|
36
|
+
│ `thatch session` CLI and the opencode-only session tools)
|
|
34
37
|
└── vector-math.ts → blobToVector + cosineSimilarity helpers
|
|
35
38
|
|
|
36
39
|
OpenCode plugin path
|
|
37
40
|
├── index.ts → plugin entry: wires DB/model/extraction, registers tools + hooks
|
|
38
41
|
├── tools.ts → thin opencode tool() wrappers over tool-defs
|
|
39
|
-
|
|
42
|
+
├── extraction.ts → in-memory ring buffer + shared payload builders
|
|
43
|
+
└── watchers.ts → in-memory watcher registry + poller (opencode-only,
|
|
44
|
+
event-driven PR notifications delivered as prompts)
|
|
40
45
|
|
|
41
46
|
MCP server path
|
|
42
47
|
├── mcp.ts → stdio JSON-RPC server: z.toJSONSchema() for tools/list,
|
|
@@ -67,6 +72,8 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
67
72
|
| `embeddings.ts` | Lazy-load the embedding model. Expose `queryEmbed`/`passageEmbed` and the model `name` (stored as an informational tag). `MockEmbeddingModel` for tests. |
|
|
68
73
|
| `extraction.ts` | Per-session in-memory ring buffer (cap 20) that buffers non-thatch tool interactions and serializes them into the JSON payload the `get_extraction_payload` tool returns. The in-memory pipeline is opencode-only, but the payload builders (`buildExtractionPayload`, `deriveTitle`) are shared by both paths — `extract-queue.ts` imports `deriveTitle`, `mcp.ts` imports `buildExtractionPayload` for the extraction payload provider. `summarizeArgs` is used internally by `buildExtractionPayload`. |
|
|
69
74
|
| `extract-queue.ts` | File-backed per-session JSONL queue (caps 20, oldest dropped). Shared by the Claude Code and Cursor hook paths, which fire one-shot per event with no cross-call state. This is the MCP-side equivalent of `extraction.ts`. |
|
|
75
|
+
| `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. |
|
|
76
|
+
| `watchers.ts` | In-memory watcher registry and background poller for event-driven notifications (opencode-only). Watches a GitHub PR 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. |
|
|
70
77
|
| `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. |
|
|
71
78
|
| `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. |
|
|
72
79
|
| `skills.ts` | `SKILL.md` content for all thatch skills, plus the installer. Skills are split into `SHARED_SKILLS` (25 skills: fact-extractor, dedup-classifier, project-primer, 8 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` (1 skill: 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]`. |
|
|
@@ -87,8 +94,8 @@ bin/thatch → CLI: stores|list|show|forget|search|mcp|reminder|hygi
|
|
|
87
94
|
| `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. |
|
|
88
95
|
| `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. |
|
|
89
96
|
| `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). |
|
|
90
|
-
| `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
|
|
91
|
-
| `dispose` |
|
|
97
|
+
| `event` | Subscribes to all session bus events. `session.created`: records `childToParent` + `parentSnapshots` (shallow copy of the parent's buffer for snapshot-aware drain), then sends the session-start reminder via `client.session.prompt` carrying the hygiene heartbeat (pending dedup pairs, stale count, orphaned branch memories) when any signal is non-zero. `session.status`: records the session's live status for the watcher delivery gate; on idle, if the session is a child, drains its snapshot, fires a toast with `childMetrics`, and deletes the child session; if the session is a parent with pending tool interactions and not already extracting, calls `triggerExtraction` to create a direct-extraction child, then flushes pending watcher events (the session just became deliverable). `session.error`: requeues the parent's buffer (child died without draining). `session.deleted`: cleans up `childToParent`, `parentSnapshots`, `childMetrics`, and `extracting`, and cancels the session's watchers. `session.compacted`: clears the compacting flag so `chat.message` nudges resume. |
|
|
98
|
+
| `dispose` | Stops the version checker and the watcher poller, closes the DB. |
|
|
92
99
|
|
|
93
100
|
Hook failures are logged with a `[thatch]` prefix — never swallowed silently.
|
|
94
101
|
Two of these hooks were dead for weeks because failures were invisible.
|
|
@@ -26,6 +26,7 @@ system, see [../skills.md](../skills.md). For non-obvious invariants, see
|
|
|
26
26
|
| [sideband.md](sideband.md) | Sideband IPC (Unix socket for MCP host hook processes) |
|
|
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
|
+
| [watchers.md](watchers.md) | Event-driven notifications (in-memory registry, background poller, PR watching) |
|
|
29
30
|
| [setup.md](setup.md) | Setup system (markers, checkSetup, auto-refresh, binary resolution) |
|
|
30
31
|
| [cli.md](cli.md) | CLI (subcommands, environment variables) |
|
|
31
32
|
| [database.md](database.md) | Database (schema, migrations, WAL, 11 tables) |
|
|
@@ -16,9 +16,12 @@ Thatch supports three host agents from a single codebase:
|
|
|
16
16
|
- **Cursor MCP**---same stdio MCP server, `--json` hook output format,
|
|
17
17
|
`mcp__thatch__` tool prefix, system prompt in AGENTS.md
|
|
18
18
|
|
|
19
|
-
All three paths
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
All three paths share the tool definitions from `src/tool-defs.ts`: 18
|
|
20
|
+
host-agnostic tools plus opencode-only tools (currently six:
|
|
21
|
+
`get_session_info`, `session_search`/`session_get`, and the
|
|
22
|
+
`watch_create`/`watch_list`/`watch_cancel` watcher tools), which the MCP server
|
|
23
|
+
filters out. The shared tools take args and a `CoreContext`
|
|
24
|
+
(`{ db, model, defaultStore }`) and return a string. The host-specific
|
|
22
25
|
wrapping---prefixing, transport, session hooks---lives outside the core.
|
|
23
26
|
|
|
24
27
|
## The three paths
|
|
@@ -147,7 +150,7 @@ an asymmetry a reviewer can catch by diffing the tool lines.
|
|
|
147
150
|
|
|
148
151
|
## Tool name prefixing
|
|
149
152
|
|
|
150
|
-
The
|
|
153
|
+
The tool definitions in `src/tool-defs.ts` use bare names:
|
|
151
154
|
`memory_remember`, `memory_recall`, `memory_list`, etc. Each host applies its
|
|
152
155
|
own prefix.
|
|
153
156
|
|
|
@@ -177,13 +180,16 @@ behavior, with some features having no MCP counterpart.
|
|
|
177
180
|
| code-review coordinator skill | Yes | No | No |
|
|
178
181
|
| Background sub-agents | Yes (experimental) | No | No |
|
|
179
182
|
| Tool batching | N/A (in-process) | PostToolBatch (batch) | postToolUse (per-tool) |
|
|
183
|
+
| `get_session_info` (session identity) | Yes | No | No |
|
|
184
|
+
| `session_search` / `session_get` (conversation archaeology) | Yes (tools) | CLI only (`thatch session ...`) | CLI only (`thatch session ...`) |
|
|
180
185
|
|
|
181
186
|
For the full parity matrix, see [../mcp-parity.md](../mcp-parity.md).
|
|
182
187
|
|
|
183
188
|
## Interactions with other features
|
|
184
189
|
|
|
185
|
-
|
|
186
|
-
`src/tool-defs.ts
|
|
190
|
+
The 18 shared tools are available on all hosts via the single source of truth
|
|
191
|
+
in `src/tool-defs.ts`; opencode-only tools (e.g. `get_session_info`) are
|
|
192
|
+
filtered out by the MCP server. Individual tool behavior is documented in
|
|
187
193
|
[memory-store.md](memory-store.md), [extraction.md](extraction.md),
|
|
188
194
|
[prediction-engine.md](prediction-engine.md),
|
|
189
195
|
[behavior-engine.md](behavior-engine.md), and
|
|
@@ -216,13 +222,13 @@ the opencode-only skills require sub-agent dispatch or in-process hooks.
|
|
|
216
222
|
| `src/setup.ts` | Setup installer for Claude Code and Cursor---markers, hooks, skills |
|
|
217
223
|
| `src/prompts.ts` | All three system prompt variants---`systemPrompt()`, `claudeInstructions()`, `cursorInstructions()` |
|
|
218
224
|
| `src/tools.ts` | Thin opencode tool wrappers---imports tool-defs, adds `thatch_` prefix via `tool()` |
|
|
219
|
-
| `src/tool-defs.ts` | Single source of truth for all
|
|
225
|
+
| `src/tool-defs.ts` | Single source of truth for all tool definitions---name, description, zod schema, execute, `opencodeOnly` flag |
|
|
220
226
|
| `bin/thatch` | CLI hook commands for MCP hosts---`reminder`, `buffer-tool`, `flush-tools`, `setup` |
|
|
221
227
|
|
|
222
228
|
## Key invariants
|
|
223
229
|
|
|
224
|
-
- All
|
|
230
|
+
- All hosts share the tool definitions from `src/tool-defs.ts` (18 shared + opencode-only tools marked `opencodeOnly: true`, which the MCP server filters out). Adding a tool requires updating `TOOL_DEFS` and all three prompt functions; opencode-only tools go in the opencode prompt only.
|
|
225
231
|
- The MCP server always exposes bare names. The `mcp__thatch__` prefix is applied by the MCP client, not thatch.
|
|
226
232
|
- The three system prompt variants are independent string constants in `src/prompts.ts` with no shared template. Editing shared prose in one requires mirroring in the other two, or they drift.
|
|
227
|
-
- The tool list line in all three prompts must match `TOOL_DEFS` in `src/tool-defs.ts
|
|
228
|
-
- opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts) have no MCP counterpart.
|
|
233
|
+
- The tool list line in all three prompts must match `TOOL_DEFS` in `src/tool-defs.ts` (opencode's list includes opencode-only tools; the MCP lists exclude them). The historical failure mode: a tool is added to `TOOL_DEFS` but only two of three prompts are updated.
|
|
234
|
+
- opencode-only features (direct extraction, compaction recovery, code-review coordinator, background sub-agents, TUI toasts, `get_session_info`, `session_search`/`session_get`, PR watchers) have no MCP counterpart. The `thatch session` CLI subcommands work anywhere - they only need an opencode.db file to read.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Watchers
|
|
2
|
+
|
|
3
|
+
Watchers are the general mechanism for event-driven notifications from
|
|
4
|
+
external sources: the model registers a watch on something outside the
|
|
5
|
+
session, thatch polls it in the background, and the plugin prompts the
|
|
6
|
+
session when a watched event happens. The first source is GitHub pull
|
|
7
|
+
requests. The registry, poller, and delivery are source-agnostic - a
|
|
8
|
+
future source only adds a fetch-and-diff function and event types.
|
|
9
|
+
|
|
10
|
+
User-facing behavior is documented in [docs/user/watchers.md](../../user/watchers.md).
|
|
11
|
+
|
|
12
|
+
## What it does
|
|
13
|
+
|
|
14
|
+
- `thatch_watch_create` / `thatch_watch_list` / `thatch_watch_cancel` tools (opencode-only)
|
|
15
|
+
- A background poller in the plugin process diffs each watcher's target
|
|
16
|
+
against its last-seen state
|
|
17
|
+
- Delivery prompts the session with a synthetic part - the same
|
|
18
|
+
mechanism opencode uses for background task completions
|
|
19
|
+
- Notifications carry pointer data only (author, URL, counts); the
|
|
20
|
+
model fetches content on demand with `gh`
|
|
21
|
+
|
|
22
|
+
## The process-lifetime decision
|
|
23
|
+
|
|
24
|
+
The registry lives in plugin memory, never in SQLite. Three reasons:
|
|
25
|
+
|
|
26
|
+
1. **Ownership is structural.** opencode loads thatch in-process, so
|
|
27
|
+
the plugin process that registered a watcher is the only process
|
|
28
|
+
that can poll and deliver it. A shared registry would need
|
|
29
|
+
claiming logic to answer "which process polls this?" - and
|
|
30
|
+
version-skewed opencode instances sharing a DB have already caused
|
|
31
|
+
corruption once.
|
|
32
|
+
2. **No orphans.** When the process exits, watcher state evaporates
|
|
33
|
+
with it. There is no cleanup path to get wrong.
|
|
34
|
+
3. **Precedent.** The extraction pipeline's opencode path is also
|
|
35
|
+
in-memory; the file-backed queue exists only for the MCP path's
|
|
36
|
+
separate short-lived hook processes.
|
|
37
|
+
|
|
38
|
+
The cost: a restart loses watches. Accepted because the session that
|
|
39
|
+
created them loses its conversational context too - a notification
|
|
40
|
+
into a session that no longer knows why it is being watched is worse
|
|
41
|
+
than a lost watch.
|
|
42
|
+
|
|
43
|
+
`session.deleted` also cancels the session's watchers and pending
|
|
44
|
+
events, and `dispose` stops the poller. These are cheap insurance;
|
|
45
|
+
process exit is the real lifetime boundary in both TUI and serve mode
|
|
46
|
+
(plugins load per-instance in the server process under
|
|
47
|
+
`opencode serve`, so watchers work there too).
|
|
48
|
+
|
|
49
|
+
## How it works
|
|
50
|
+
|
|
51
|
+
### Registration (`watch_create`)
|
|
52
|
+
|
|
53
|
+
`create()` immediately fetches the PR's state as a baseline via
|
|
54
|
+
`fetchPrState()` - four `gh api` calls (the pull, issue comments,
|
|
55
|
+
review comments, check runs on the head SHA), three of them in
|
|
56
|
+
parallel. The baseline doubles as validation: a missing PR, wrong
|
|
57
|
+
repo, or broken gh setup fails registration with a real error instead
|
|
58
|
+
of producing a watcher that never fires. Watch creation is also gated
|
|
59
|
+
on a cached `gh --version` availability check.
|
|
60
|
+
|
|
61
|
+
### Polling
|
|
62
|
+
|
|
63
|
+
A `setInterval` loop (unref'd - it never keeps the process alive)
|
|
64
|
+
runs `poll()` every 60 seconds (configurable). Each cycle:
|
|
65
|
+
fetch the current state, run `diffPrState()` against the watcher's
|
|
66
|
+
last-seen state, filter events to the watched types, queue them, and
|
|
67
|
+
attempt delivery. Poll errors are per-watcher; one bad PR never
|
|
68
|
+
blocks the others. The diff is a pure function, so it is unit-tested
|
|
69
|
+
without any network access.
|
|
70
|
+
|
|
71
|
+
Event detection:
|
|
72
|
+
|
|
73
|
+
- comments by id monotonicity (fetch is sorted descending)
|
|
74
|
+
- commits by head SHA change
|
|
75
|
+
- status/merged by field change
|
|
76
|
+
- description by SHA-256 of the body text (the body itself is never
|
|
77
|
+
stored)
|
|
78
|
+
- CI by check-run transitions into a completed status
|
|
79
|
+
|
|
80
|
+
A diff emits at most 10 events per watcher per cycle, so comment
|
|
81
|
+
floods collapse into one batch.
|
|
82
|
+
|
|
83
|
+
### Delivery
|
|
84
|
+
|
|
85
|
+
Events queue in an in-memory pending map keyed by session. Delivery
|
|
86
|
+
is gated by a `canDeliver` predicate the plugin supplies: the session
|
|
87
|
+
must be idle (tracked from `session.status` events) and not
|
|
88
|
+
compacting. Proactive prompts are never injected into a running turn.
|
|
89
|
+
|
|
90
|
+
When the gate is open, the plugin calls `client.session.promptAsync`
|
|
91
|
+
on the session with a synthetic text part built by
|
|
92
|
+
`watcherNotificationNudge()`. This triggers a full model turn - the
|
|
93
|
+
model reads the notification and decides whether to act. The
|
|
94
|
+
notification text borrows the background-task-completion framing
|
|
95
|
+
(system event, not user input, not approval to advance other work),
|
|
96
|
+
following the same load-bearing wording discipline as the extraction
|
|
97
|
+
nudge.
|
|
98
|
+
|
|
99
|
+
Failed or gated deliveries stay pending and retry on the next poll
|
|
100
|
+
cycle or when the session next goes idle (the event hook calls
|
|
101
|
+
`deliverPending()` there).
|
|
102
|
+
|
|
103
|
+
### Event vocabulary
|
|
104
|
+
|
|
105
|
+
`pr_comment`, `pr_review_comment`, `pr_review_reply`, `pr_commit`,
|
|
106
|
+
`pr_status`, `pr_description`, `pr_ci`. The tool's `events` argument
|
|
107
|
+
selects a subset; the default is all seven.
|
|
108
|
+
|
|
109
|
+
## Interactions with other features
|
|
110
|
+
|
|
111
|
+
- Extraction pipeline ([extraction.md](extraction.md)): the
|
|
112
|
+
notification turn's tool calls are buffered and extracted like any
|
|
113
|
+
other turn. Watch tools themselves are `thatch_*`-prefixed and
|
|
114
|
+
excluded from buffering.
|
|
115
|
+
- Session lifecycle ([session-lifecycle.md](session-lifecycle.md)):
|
|
116
|
+
`session.status` feeds the delivery gate; `session.deleted`
|
|
117
|
+
cancels watchers.
|
|
118
|
+
- Multi-host ([multi-host.md](multi-host.md)): opencode-only. MCP
|
|
119
|
+
hosts have no poller, no event bus, and no channel to start a turn
|
|
120
|
+
proactively. A future CLI daemon could close this gap.
|
|
121
|
+
|
|
122
|
+
## Configuration
|
|
123
|
+
|
|
124
|
+
- `THATCH_WATCH_POLL_SECONDS` - poll interval (default 60)
|
|
125
|
+
- `THATCH_WATCH_TTL_MINUTES` - watcher time-to-live (default 480)
|
|
126
|
+
- `THATCH_WATCH_MAX_PER_SESSION` - active watchers per session
|
|
127
|
+
(default 5)
|
|
128
|
+
|
|
129
|
+
## Source files
|
|
130
|
+
|
|
131
|
+
- `src/watchers.ts` - registry, poller, gh CLI runner, diff functions
|
|
132
|
+
- `src/tool-defs.ts` - the three watch tool definitions
|
|
133
|
+
- `src/index.ts` - registry construction, delivery closure,
|
|
134
|
+
session.status tracking, session.deleted cleanup, dispose
|
|
135
|
+
- `src/prompts.ts` - `watcherNotificationNudge()`, system prompt
|
|
136
|
+
Watchers section
|
|
137
|
+
- `tests/watchers.test.ts` - registry, poll, and diff unit tests
|
|
138
|
+
(mocked gh, no network)
|
|
139
|
+
- `tests/qa/auto/uc-095-watchers.ts` - end-to-end lifecycle against a
|
|
140
|
+
mocked gh runner
|
|
141
|
+
|
|
142
|
+
## Adding a new source type
|
|
143
|
+
|
|
144
|
+
The registry is source-agnostic in shape but currently hardcoded to
|
|
145
|
+
the github-pr fetch/diff pair. A new source type means: new event
|
|
146
|
+
types in `WATCHER_EVENT_TYPES`, a fetch/diff function pair, a
|
|
147
|
+
`source_type` discriminator on `Watcher` (add it when the second
|
|
148
|
+
source arrives - do not parameterize early), and the tool surface for
|
|
149
|
+
registering it. Keep the pointer-only notification rule: external
|
|
150
|
+
content enters the context on explicit fetch, never via notification.
|
package/docs/dev/mcp-parity.md
CHANGED
|
@@ -18,7 +18,7 @@ This document maps the feature parity and documents the remaining gaps.
|
|
|
18
18
|
|
|
19
19
|
| Feature | OpenCode plugin | Claude Code MCP + hooks | Cursor MCP + hooks | Parity |
|
|
20
20
|
|---------|----------------|------------------------|---------------------|--------|
|
|
21
|
-
| **Tools** (18, single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix | **Full** |
|
|
21
|
+
| **Tools** (18 shared + 6 opencode-only, single source of truth) | Plugin tool registration, `thatch_` prefix | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | MCP `tools/list` + `tools/call`, `mcp__thatch__` prefix (opencode-only tools filtered out) | **Full** for the 18 shared tools; `get_session_info` (session identity) and `session_search`/`session_get` (conversation archaeology) are opencode-only — MCP hosts have no session concept. The `thatch session` CLI subcommands cover the same archaeology on any host with an opencode.db file. The `watch_*` tools (PR watchers) are opencode-only — MCP hosts have no poller or proactive-prompt channel |
|
|
22
22
|
| **System prompt** (store names, usage rules) | `experimental.chat.system.transform` — dynamic, repo baked in at runtime | CLAUDE.md static text appended by `thatch setup`; repo auto-detected by the MCP server at startup | AGENTS.md static text appended by `thatch setup`; repo auto-detected at startup | **Approximate** — static text persists through compaction, so the agent retains the usage instructions, but the dynamic per-turn refresh is lost |
|
|
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 |
|