@jeffober/thatch 0.1.13 → 0.1.15

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 CHANGED
@@ -32,6 +32,17 @@ No API keys, no cloud services; everything runs on your machine.
32
32
  On next start, OpenCode npm-installs thatch (putting the `thatch` CLI on your
33
33
  PATH as a side effect) and its tools are available immediately.
34
34
 
35
+ Set this environment variable so the extraction nudge can run the fact-extractor
36
+ as a **background** sub-agent instead of blocking your turn:
37
+
38
+ ```bash
39
+ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
40
+ ```
41
+
42
+ Without it, opencode hides the task tool's `background` parameter and every
43
+ extraction nudge runs synchronously. Put it in your shell rc, `mise.toml`
44
+ `[env]`, or `.envrc`.
45
+
35
46
  **Self-hosting** (local development):
36
47
 
37
48
  ```jsonc
@@ -10,13 +10,21 @@ This entire review is a static analysis exercise. Do NOT run tests, linters, com
10
10
 
11
11
  ## Step 1: Resolve the review target
12
12
 
13
+ First, run `git fetch origin` (NOT `git pull` — fetch updates remote-tracking refs without touching the working tree or current branch). This refreshes `origin/main` and other remote-tracking refs to their true remote state. Local branches (e.g. `main`) only advance on checkout/pull and may be stale on a long-lived feature checkout, so resolve the base against the remote tracking ref, not the local branch.
14
+
13
15
  Identify what to review:
14
- - If a branch was specified, identify the base branch (usually main or master), compute the merge-base, and review the range merge-base..HEAD.
15
- - If a PR number was specified, use gh pr view to resolve the head and base, fetch both refs, and review merge-base..head.
16
+ - If a branch was specified, identify the base branch (usually main or master), compute the merge-base against the remote tracking ref (e.g. `git merge-base HEAD origin/main`), and review the range merge-base..HEAD.
17
+ - If a PR number was specified, use `gh pr view` to resolve the head and base, fetch both refs, and review merge-base..head.
16
18
  - If a git range was specified (A..B form), use it directly.
17
- - If no target was specified, review the current branch against its base.
19
+ - If no target was specified, review the current branch against its base, resolving the base from `origin/main` (or the appropriate remote tracking ref) after fetching.
20
+
21
+ Also detect whether the current branch has a **connected PR/MR** even when none was specified — this is what enables follow-up-round detection in Step 2. Inspect `git remote -v` to recognize the VCS host (github.com → GitHub/`gh`; gitlab.* → GitLab/`glab`; bitbucket.org → Bitbucket Cloud; dev.azure.com → Azure DevOps). Then probe for an open PR/MR on the current branch using the matching CLI or REST API:
22
+ - GitHub: `gh pr view --head <branch> --json number,headRefOid,baseRefOid`
23
+ - GitLab: `glab mr list --source-branch <branch>`
24
+ - Other providers: equivalent detection via their CLI or REST API.
25
+ - If you cannot detect or cannot interact with the VCS (no CLI, no REST access), treat the change as a **local-branch review** and proceed with the existing procedure. A local-branch review is not a defect — it just means there is no prior-comments register to build.
18
26
 
19
- Fetch any refs not locally reachable so branches and PRs that were never checked out can be reviewed.
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).
20
28
 
21
29
  Run git diff --stat on the resolved range and git log --oneline to understand the change.
22
30
 
@@ -30,8 +38,9 @@ Load the thatch-review-context skill and follow its methodology to build a conte
30
38
  - thatch_memory_recall for feature/project status, design decisions, ticket dependencies
31
39
  - Project docs, READMEs, design docs
32
40
  - Issue tracker (gh issue/view) if ticket IDs are found
41
+ - **Prior review comments** — if Step 1 detected a connected PR/MR, fetch ALL prior review comments on it (see source #8 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`.
33
42
 
34
- Produce the context brief in the format the skill prescribes (project context, this change's scope, deferred work, dependencies, relevant constraints).
43
+ 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).
35
44
 
36
45
  If context is sparse (no PR description, no ticket references, no relevant memories), note that the change appears standalone. Even a sparse brief is valuable: it tells specialists you looked and found nothing, so they do not need to repeat the search.
37
46
 
@@ -99,13 +108,16 @@ After all sub-agents complete, load the thatch-review-synthesizer skill to verif
99
108
 
100
109
  Confirmed LOW findings are mandatory in the final report, including mechanical findings (pedantic, no-slop, breadcrumbs, docs, naming, style, comments). Do not omit them for being non-functional.
101
110
 
111
+ **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.
112
+
102
113
  Alternatively, perform the synthesis yourself:
103
114
  1. Read each finding's cited location to verify evidence accuracy.
104
115
  2. Deduplicate findings flagged by multiple specialists.
105
116
  3. Group findings by root cause where multiple findings stem from the same issue.
106
117
  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.
107
- 5. Calibrate severity (BLOCKING > HIGH > MEDIUM > LOW) based on your verification.
108
- 6. Produce a final report grouped by severity, with coverage gaps noted. Include every confirmed LOW finding.
118
+ 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.
119
+ 6. Calibrate severity (BLOCKING > HIGH > MEDIUM > LOW) based on your verification.
120
+ 7. Produce a final report grouped by severity, with coverage gaps noted. Include every confirmed LOW finding.
109
121
 
110
122
  ## Specialist briefing template
111
123
 
@@ -116,6 +128,7 @@ When dispatching each sub-agent, include in the prompt:
116
128
  - The diff stat for this unit's files
117
129
  - **The project context brief** from Step 2, filtered to what is relevant to this unit's scope. Explicitly list any deferred work that falls within this unit's files, and call out TODO ($ticket) markers the specialists should recognize as intentional.
118
130
  - **The workflow guide** from Step 3, filtered to the workflows relevant to this unit's scope. Use it to understand the purpose and evolution of the code before flagging issues. It provides the code-level context (flows, contracts, history, constraints) that prevents false positives about intentional behavior and long-standing design decisions.
131
+ - **The prior review comments register** from Step 2 when present — the list of issues other reviewers already raised, with each comment's current-HEAD location and preliminary status. This is a **hint list, not a finding list**: it saves specialists the work of treating an already-identified issue as newly discovered. Specialists still produce their own findings normally. For any finding that matches a prior comment, set `Provenance` to `previously identified by @author, PR #N, DATE` in addition to the branch-introduced / pre-existing classification — the synthesizer does the final cross-reference and dedup.
119
132
  - Any design context or specific concerns
120
133
  - Explicit scope boundaries ("your scope is X; do NOT review Y")
121
134
  - Instruction to produce markdown findings with: severity, category, file:line, finding, evidence, trigger scenario, reachability, source of truth, producer chain, provenance
@@ -56,6 +56,62 @@ If ticket IDs were found:
56
56
  - Look for milestone assignments, dependencies, and blocking relationships
57
57
  - Check for epic or parent issues that describe the overall feature
58
58
 
59
+ ### 8. Prior review comments (follow-up round detection)
60
+
61
+ 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
+
63
+ #### Detect the VCS provider
64
+
65
+ `git remote -v` lists remotes and URLs. Parse the upstream URL:
66
+ - `github.com` → GitHub (`gh`)
67
+ - `gitlab.*` → GitLab (`glab`)
68
+ - `bitbucket.org` → Bitbucket Cloud
69
+ - `dev.azure.com` → Azure DevOps
70
+ - Unknown host or no upstream → local-branch review, skip this source.
71
+
72
+ If the matching CLI tool (`gh`, `glab`, etc.) is not on PATH and you cannot call the provider's REST API via available HTTP tools, skip this source — the missing integration is not a finding.
73
+
74
+ #### Detect the connected PR/MR
75
+
76
+ Use the current branch name (without remote or merge-base prefix) to look up an open PR/MR:
77
+ - **GitHub**: `gh pr view --head <branch> --json number,title,state,headRefName,baseRefName,headRefOid,baseRefOid`. Fallback: `gh api repos/{owner}/{repo}/pulls --jq ".[] | select(.head.ref==\"<branch>\")"`.
78
+ - **GitLab**: `glab mr list --source-branch <branch>`.
79
+ - **Bitbucket Cloud**: REST `https://api.bitbucket.org/2.0/repositories/<owner>/<slug>/pullrequests?q=source.branch.name="<branch>"` (no widely-adopted CLI; call via available HTTP tool).
80
+ - **Azure DevOps**: `az repos pr list --source-branch <branch> --org <org-url> -r <project>`.
81
+
82
+ If a PR/MR is found and has zero prior review comments/reviews by other users, treat this as the first round on this PR/MR — skip the addressed-check, but still record the PR/MR identity in the brief for reference.
83
+
84
+ #### Fetch ALL prior review comments
85
+
86
+ Pull review threads and summaries regardless of resolved state — the goal is to see everything that has been raised, not just unresolved threads.
87
+ - **GitHub**: `gh api repos/{owner}/{repo}/pulls/{N}/reviews` (review summaries: body, user, state, submitted_at) AND `gh api repos/{owner}/{repo}/pulls/{N}/comments` (inline file:line comments with `path`, `line`, `original_line`, `commit_id`, `body`, `user`).
88
+ - **GitLab**: `glab api projects/:id/merge_requests/:iid/discussions` returns discussion threads with anchored positions; threads expose `resolved` and `resolved_by` (use as a supplementary addressed signal).
89
+ - **Bitbucket Cloud**: REST `activities` endpoint for the PR.
90
+ - **Azure DevOps**: REST `threads` endpoint for the PR.
91
+
92
+ #### Build the prior-comments register
93
+
94
+ For each prior comment, record:
95
+ - Author, submitted date
96
+ - Original commit SHA, file, line range (or `summary` for non-inline review)
97
+ - Comment body (quoted)
98
+ - The semantic claim raised (what the reviewer said was wrong — paraphrase the core issue, one sentence)
99
+ - VCS resolve state if available (GitLab `resolved`/`resolved_by`; GitHub `isResolved` via GraphQL if accessible)
100
+
101
+ #### Addressed-check methodology
102
+
103
+ For each comment, attempt to determine whether it has been addressed in the **current HEAD** under review:
104
+ 1. **Locate the original code location.** If the file was deleted, mark `unclear`. If the file was renamed, follow it with `git log --diff-filter=R -- <path>`.
105
+ 2. **Anchor to current HEAD.** Read the cited line plus 3-5 lines of context at the original commit (`git show <comment-SHA>:<file>`). Find the equivalent region in the current HEAD file by matching the surrounding text (search for unique tokens from that region). Determine the new line range.
106
+ 3. **Judge the issue.** Read the code at the new location. Does the issue the comment described still exist, or has it been fixed, refactored away, or otherwise made moot by changes in this change's commit range?
107
+ 4. **Tag preliminary status:**
108
+ - `addressed` — the code at the equivalent location no longer exhibits the issue, or the referenced structure was refactored away in a way that resolves the comment.
109
+ - `still active` — the issue persists at the equivalent location.
110
+ - `unclear` — file gone, line unlocatable, or the substantive change makes comparison impossible.
111
+ 5. **VCS resolve signal as a supplement, not a substitute.** If the VCS exposes a resolved flag (GitLab `resolved: true`, GitHub `isResolved` via GraphQL), record it as supporting evidence. When the resolved flag disagrees with the code-state verdict, trust the code state — a thread can be marked resolved in the UI while the bug persists, and an unresolved thread may already have been fixed by a later commit. Note both signals in the register so the synthesizer can weigh them.
112
+
113
+ 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.
114
+
59
115
  ## Context brief format
60
116
 
61
117
  Produce a structured brief:
@@ -83,6 +139,19 @@ List each deferred piece:
83
139
  - Design decisions or architectural constraints from memories or docs
84
140
  - Conventions that reviewers should be aware of
85
141
 
142
+ ### Prior review comments (only when the change has a connected PR/MR with prior reviews)
143
+
144
+ List the prior-comments register (one entry per prior comment):
145
+ - Author and submitted date
146
+ - Original location (`file:line @ commit-SHA`, or `summary review`) and current HEAD location (`file:line`) if locatable
147
+ - The semantic claim raised (one-sentence paraphrase)
148
+ - Preliminary status: `addressed` / `still active` / `unclear` — with the code-state evidence (a short quote from the current HEAD that supports the verdict)
149
+ - VCS resolve flag if applicable (and any disagreement with the code-state verdict)
150
+
151
+ If context gathering found a connected PR/MR but no prior reviews by other users, state `First round on PR #N — no prior comments to register`.
152
+
153
+ If context gathering found no connected PR/MR (local-branch review), state `Local-branch review — no prior-comment fetch`. This is not a gap; the existing review procedure applies.
154
+
86
155
  ## If context is sparse
87
156
 
88
157
  Not every PR will have rich context. If you find minimal context:
@@ -54,6 +54,26 @@ The same issue may be flagged by multiple specialists when it spans category bou
54
54
 
55
55
  Multiple findings may stem from the same underlying issue (e.g., a contract mismatch causes errors in 3 call sites). Group these under the root cause.
56
56
 
57
+ ## Cross-reference against prior review comments
58
+
59
+ If your input includes a **prior-comments register** (built by the coordinator via `thatch-review-context` source #8 — 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.
60
+
61
+ For each new finding:
62
+ 1. **Search for a matching prior comment** by location and semantic claim. Match if both hold:
63
+ - The finding's cited file:line falls within (or has moved to) the prior comment's referenced location in current HEAD, AND
64
+ - The finding's core claim restates the prior comment's semantic claim (the same code-level issue, even if worded differently or covering a slightly wider scope).
65
+ 2. **If a match is found**, tag the finding's `Provenance` field as `previously identified by @author, PR #N, DATE` in addition to its branch-introduced / pre-existing classification. Do not drop the finding. Keep it in the main findings (with attribution) so the user sees what still needs attention this round.
66
+
67
+ For each entry in the prior-comments register, finalize its status using both the register's preliminary verdict and this round's findings:
68
+ - **`addressed`** — the register's preliminary status was `addressed` AND no new finding reproduces the comment. Re-read the cited current-HEAD location to confirm the issue is gone; if you find the issue persists, override to `still active — not reproduced this round, re-verified above`.
69
+ - **`still active — reproduced by finding X`** — a new finding this round matches the prior comment. Cite the finding's severity and source specialist.
70
+ - **`still active — not reproduced this round, re-verified above`** — no new finding matches, but you re-read the code and the issue described in the prior comment still persists. Include a one-line quote from current HEAD as evidence.
71
+ - **`unclear`** — the original line is unlocatable in current HEAD, the file is gone, or substantive change makes the comparison impossible. State which.
72
+
73
+ New findings that match a prior comment tagged `addressed` (preliminary) are still reported — they may indicate the resolution was incomplete or that the issue regressed. Note this in the finding's `Provenance` field so the user investigates.
74
+
75
+ When the VCS resolve flag (GitLab `resolved`, GitHub `isResolved`) disagrees with the code-state verdict (e.g., thread marked resolved in the UI but the bug persists), trust the code state and call out the disagreement in the appendix entry.
76
+
57
77
  ## Severity calibration
58
78
 
59
79
  Assign final severity based on YOUR verification:
@@ -82,7 +102,7 @@ For each finding, grouped by severity (BLOCKING > HIGH > MEDIUM > LOW). Each fin
82
102
  4. **Finding**: what the problem is
83
103
  5. **Evidence**: the code you read to confirm it (quote the exact lines you verified)
84
104
  6. **Trigger/Proof**: the workflow trigger, and for state/data/behavior issues the producer then transform then consumer chain you verified
85
- 7. **Provenance**: branch-introduced or pre-existing
105
+ 7. **Provenance**: branch-introduced or pre-existing. If the finding matches a prior review comment, append `; previously identified by @author, PR #N, DATE` to this field. For findings matching an `addressed` prior comment, also add a one-line note on why the finding persists so the user can investigate whether the prior resolution is incomplete or the issue has regressed.
86
106
 
87
107
  ### Rejected findings (appendix, brief)
88
108
  Findings you rejected and a one-line reason why. Include the specialist and location for each.
@@ -90,5 +110,19 @@ Findings you rejected and a one-line reason why. Include the specialist and loca
90
110
  ### Pre-existing bugs (appendix, brief)
91
111
  Findings you verified as real but pre-existing, with a one-line note on the issue and its potential impact.
92
112
 
113
+ ### Previously identified findings (appendix — only when a prior-comments register was provided)
114
+ List every entry from the prior-comments register here. For each entry, produce a one-block record:
115
+ 1. **Author and date** of the original comment
116
+ 2. **Original location** (`file:line @ commit-SHA`, or `summary review`) and **current HEAD location** (`file:line`) if locatable
117
+ 3. **Claim**: one-sentence paraphrase of what the prior reviewer raised
118
+ 4. **Final status**: one of
119
+ - `addressed` — issue confirmed gone in current HEAD
120
+ - `still active — reproduced by finding X` — with the cross-referenced finding's severity and source specialist
121
+ - `still active — not reproduced this round, re-verified above` — with a one-line quote from current HEAD as evidence
122
+ - `unclear` — with the reason (file gone, line unlocatable, etc.)
123
+ 5. **VCS resolve flag** if applicable, and any disagreement with the code-state verdict (e.g., thread marked resolved in the UI but the bug persists) — trust the code state and surface the disagreement.
124
+
125
+ If no prior-comments register was provided (local-branch review or first round on the PR/MR), state `None — no prior review comments to cross-reference` and move on.
126
+
93
127
  ### Coverage gaps
94
128
  Note which files or areas were NOT covered by any specialist.
@@ -36,8 +36,8 @@ description: Extract durable project facts ... Use when ...
36
36
  | `thatch-review-no-slop` | AI writing anti-patterns: change narration, fourth-wall breaks, em dashes, filler. |
37
37
  | `thatch-review-breadcrumbs` | Comment narrative: do comments form a coherent outline of behavior? |
38
38
  | `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
39
- | `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. |
40
- | `thatch-review-context` | Gather project/feature context (PR descriptions, git archaeology, ticket references, memory) before fan-out. Prevents false positives about intentionally deferred work. |
39
+ | `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. Cross-references findings against prior review comments when a follow-up round register is provided; produces a Previously identified appendix. |
40
+ | `thatch-review-context` | Gather project/feature context (PR descriptions, git archaeology, ticket references, memory) before fan-out. Prevents false positives about intentionally deferred work. Also fetches prior review comments on a connected PR/MR for follow-up-round detection and builds a register with preliminary addressed-check status per comment. |
41
41
  | `thatch-workflow-research` | Research code workflows/features affected by a change or planned change. Reads code flows, comments, git history, memories, docs. Produces a guide to the code for reviewers or planners. |
42
42
  | `thatch-session-reflection` | End-of-session memory recording (project, user, tools, self). |
43
43
 
@@ -45,7 +45,7 @@ description: Extract durable project facts ... Use when ...
45
45
 
46
46
  | Skill | Role |
47
47
  |-------|------|
48
- | `thatch-code-review` | Resolve review target, gather project context, research affected workflows, estimate complexity, partition, dispatch the 6 specialists in parallel, synthesize. |
48
+ | `thatch-code-review` | Resolve review target (incl. VCS detection and connected PR/MR lookup for follow-up round detection), gather project context, research affected workflows, estimate complexity, partition, dispatch the 6 specialists in parallel, synthesize (incl. cross-referencing findings against prior review comments). |
49
49
 
50
50
  ## REVIEW_COMMON
51
51
 
@@ -20,6 +20,17 @@ Publish to npm and add thatch to your opencode config:
20
20
 
21
21
  OpenCode installs the plugin and its dependencies automatically on next start.
22
22
 
23
+ Set this environment variable so the extraction nudge can dispatch the
24
+ fact-extractor as a **background** sub-agent instead of blocking your turn:
25
+
26
+ ```bash
27
+ export OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true
28
+ ```
29
+
30
+ Without it, opencode hides the task tool's `background` parameter and the
31
+ extraction nudge runs synchronously. Put it in your shell rc, `mise.toml`
32
+ `[env]`, or `.envrc`.
33
+
23
34
  For local development before publishing, use a file path:
24
35
 
25
36
  ```jsonc
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Persistent memory for AI coding agents — local embeddings, SQLite stores, zero config. Works with OpenCode (as a plugin), Claude Code (as an MCP server), and Cursor (as an MCP server).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -56,7 +56,7 @@ export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void
56
56
  const additions: ToolInteraction[] = [];
57
57
  for (const tc of toolCalls) {
58
58
  const lower = tc.tool_name.toLowerCase();
59
- if (lower === "mcp__thatch__memory_remember") {
59
+ if (lower === "mcp__thatch__memory_remember" || lower === "mcp__thatch__extraction_done") {
60
60
  resetMissedCount(sessionID);
61
61
  consumeQueue(sessionID);
62
62
  existing.length = 0;
package/src/index.ts CHANGED
@@ -56,6 +56,14 @@ export const server: Plugin = async ({ client, worktree }) => {
56
56
  // all-caps shouting. Reset to 0 whenever the agent writes a memory.
57
57
  const missedNudges = new Map<string, number>();
58
58
 
59
+ // Parent-child session mapping for cross-session buffer drain. opencode
60
+ // creates real child sessions with their own IDs for sub-agents; without
61
+ // this map, a memory_remember call in a child (e.g. a background
62
+ // fact-extractor task) drains the child's empty buffer but leaves the
63
+ // parent's buffer untouched, causing the nudge to replay indefinitely.
64
+ // Populated from session.created events that carry a parentID.
65
+ const childToParent = new Map<string, string>();
66
+
59
67
  // Skills always install to the global opencode config — installing into the
60
68
  // worktree would mutate the user's repo (untracked files in git status).
61
69
  // A failed install degrades the nudge workflow but must not kill the plugin.
@@ -103,8 +111,34 @@ export const server: Plugin = async ({ client, worktree }) => {
103
111
  // The buffer is NOT drained on nudge delivery — it persists until the
104
112
  // agent writes a memory, so ignored nudges accumulate and the payload
105
113
  // grows with each missed cycle.
114
+ //
115
+ // Fix A: a memory_remember call in a child session (sub-agent) also
116
+ // drains the parent's buffer. Without this, dispatching the
117
+ // fact-extractor as a background task writes memories in the child
118
+ // but never clears the parent's queue — the nudge replays every turn.
119
+ //
120
+ // Fix C: thatch_extraction_done is an explicit acknowledgment tool the
121
+ // model calls in the parent session after dispatching. Belt-and-
122
+ // suspenders for fix A: covers the case where the sub-agent errors out
123
+ // or the host doesn't expose parent-child session relationships.
106
124
  "tool.execute.after": async (input, output) => {
107
125
  if (input.tool === "thatch_memory_remember") {
126
+ extraction.consume(input.sessionID);
127
+ missedNudges.delete(input.sessionID);
128
+ // Fix A: if this is a child session, also drain the parent so the
129
+ // parent's extraction nudge stops replaying after a sub-agent writes
130
+ // memories on its behalf.
131
+ const parentID = childToParent.get(input.sessionID);
132
+ if (parentID) {
133
+ extraction.consume(parentID);
134
+ missedNudges.delete(parentID);
135
+ }
136
+ return;
137
+ }
138
+ // Fix C: explicit acknowledgment tool drains the buffer without
139
+ // requiring a memory write. The model calls this after dispatching
140
+ // the fact-extractor to a sub-agent.
141
+ if (input.tool === "thatch_extraction_done") {
108
142
  extraction.consume(input.sessionID);
109
143
  missedNudges.delete(input.sessionID);
110
144
  return;
@@ -183,7 +217,22 @@ export const server: Plugin = async ({ client, worktree }) => {
183
217
 
184
218
  // 5. Session-start reminder, carrying the hygiene heartbeat. Hygiene is
185
219
  // best-effort: a failure there must not cost the reminder itself.
220
+ //
221
+ // Also tracks parent-child session relationships for fix A (cross-session
222
+ // buffer drain). session.created with a parentID records the mapping;
223
+ // session.deleted cleans it up to avoid unbounded growth.
186
224
  event: async ({ event }) => {
225
+ if (event.type === "session.created") {
226
+ const info = event.properties.info;
227
+ if (info.parentID) {
228
+ childToParent.set(info.id, info.parentID);
229
+ }
230
+ }
231
+ if (event.type === "session.deleted") {
232
+ childToParent.delete(event.properties.info.id);
233
+ return;
234
+ }
235
+
187
236
  if (event.type !== "session.created") return;
188
237
  const id = event.properties.info.id;
189
238
 
package/src/prompts.ts CHANGED
@@ -136,7 +136,7 @@ Tools are prefixed in Claude Code: \`mcp__thatch__memory_remember\`,
136
136
  \`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
137
137
  \`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
138
138
  \`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
139
- \`mcp__thatch__dedup_mark_checked\`.
139
+ \`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`.
140
140
  Bare names used below for readability.
141
141
 
142
142
  ## Stores
@@ -268,21 +268,38 @@ export function extractionNudge(
268
268
  payload: string,
269
269
  ): string {
270
270
  const plural = count === 1 ? "" : "s";
271
+ // opencode's task tool has a `background` parameter (requires
272
+ // OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true) that launches the sub-agent
273
+ // asynchronously — the call returns immediately and the session is notified
274
+ // on completion. Naming the parameter gives the model a direct hit on the tool
275
+ // schema it sees. MCP hosts (Claude Code, Cursor) keep generic sub-agent
276
+ // wording because their background mechanisms differ and are not exposed as a
277
+ // per-call parameter in the same way.
278
+ const isOpencode = toolName.startsWith("thatch_");
279
+ const dispatch = isOpencode
280
+ ? "Dispatch a task with background: true"
281
+ : "Spawn a background sub-agent";
282
+ const drainTool = isOpencode ? "thatch_extraction_done" : "mcp__thatch__extraction_done";
283
+ const acknowledge = `After dispatching, call ${drainTool} to acknowledge.`;
284
+
271
285
  if (missedCount >= 3) {
272
286
  return `[thatch] YOU ARE IGNORING EXTRACTION INSTRUCTIONS. ` +
273
287
  `${count} TOOL INTERACTION${plural.toUpperCase()} ARE QUEUED AND YOU HAVE NOT PROCESSED ANY OF THEM. ` +
274
288
  `THE USER INSTALLED THIS PLUGIN FOR A REASON. ` +
275
- `SPAWN A SUB-AGENT NOW TO RUN THE thatch-fact-extractor SKILL ` +
276
- `AND USE ${toolName.toUpperCase()} TO SAVE DURABLE FACTS FROM THIS PAYLOAD:\n${payload}`;
289
+ `${dispatch.toUpperCase()} NOW TO RUN THE thatch-fact-extractor SKILL. ` +
290
+ `${acknowledge.toUpperCase()} ` +
291
+ `USE ${toolName.toUpperCase()} TO SAVE DURABLE FACTS FROM THIS PAYLOAD:\n${payload}`;
277
292
  }
278
293
  if (missedCount >= 2) {
279
294
  return `[thatch] YOU HAVE NOT PROCESSED YOUR PREVIOUS EXTRACTION PAYLOAD${plural.toUpperCase()}. ` +
280
295
  `${count} more tool interaction${plural} are now queued. ` +
281
- `Spawn a background sub-agent to run the thatch-fact-extractor skill on this payload. ` +
296
+ `${dispatch} to run the thatch-fact-extractor skill on this payload. ` +
297
+ `${acknowledge} ` +
282
298
  `Use ${toolName} to save durable facts from this payload:\n${payload}`;
283
299
  }
284
- return `[thatch] Spawn a background sub-agent to run the thatch-fact-extractor skill ` +
300
+ return `[thatch] ${dispatch} to run the thatch-fact-extractor skill ` +
285
301
  `on ${count} queued tool interaction${plural}. ` +
302
+ `${acknowledge} ` +
286
303
  `Then answer the user. ` +
287
304
  `Use ${toolName} to save durable facts from this payload:\n${payload}`;
288
305
  }
@@ -312,7 +329,7 @@ Tools are prefixed in Cursor: \`mcp__thatch__memory_remember\`,
312
329
  \`mcp__thatch__memory_recall\`, \`mcp__thatch__memory_list\`,
313
330
  \`mcp__thatch__memory_show\`, \`mcp__thatch__memory_forget\`,
314
331
  \`mcp__thatch__store_list\`, \`mcp__thatch__find_duplicates\`,
315
- \`mcp__thatch__dedup_mark_checked\`.
332
+ \`mcp__thatch__dedup_mark_checked\`, \`mcp__thatch__extraction_done\`.
316
333
  Bare names used below for readability.
317
334
 
318
335
  ## Stores
package/src/tool-defs.ts CHANGED
@@ -340,6 +340,33 @@ const markCheckedDef: ToolDef = {
340
340
  },
341
341
  };
342
342
 
343
+ /**
344
+ * Acknowledge that the extraction nudge has been handled. Drains the
345
+ * extraction buffer so the nudge does not replay on the next turn.
346
+ *
347
+ * The actual draining happens in the host's post-tool hook
348
+ * (tool.execute.after for opencode, PostToolBatch/appendBatch for MCP) —
349
+ * this tool's execute function is a no-op confirmation. The tool exists so
350
+ * the model can call it in the parent session after dispatching the
351
+ * fact-extractor to a sub-agent, giving the hook a recognizable tool name
352
+ * to key on. Without it, the buffer only drains when a memory_remember
353
+ * call lands in the same session (or a tracked child session), which fails
354
+ * if the sub-agent errors out or the host doesn't expose parent-child
355
+ * session relationships.
356
+ */
357
+ const extractionDoneDef: ToolDef = {
358
+ name: "extraction_done",
359
+ description:
360
+ "Acknowledge that the extraction nudge has been dispatched to a sub-agent. " +
361
+ "Drains the extraction buffer so the nudge does not replay. " +
362
+ "Call this in the parent session after dispatching a background task " +
363
+ "to run the thatch-fact-extractor skill.",
364
+ args: {},
365
+ async execute() {
366
+ return "[acknowledged] extraction buffer drained";
367
+ },
368
+ };
369
+
343
370
  /**
344
371
  * All tool definitions, in the order they should be presented to the agent.
345
372
  * The opencode plugin wraps each in `tool()`; the MCP server exposes them
@@ -354,4 +381,5 @@ export const TOOL_DEFS: ToolDef[] = [
354
381
  listStoresDef,
355
382
  findDuplicatesDef,
356
383
  markCheckedDef,
384
+ extractionDoneDef,
357
385
  ];