@jeffober/thatch 0.2.1 → 0.2.3

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.
@@ -67,32 +67,26 @@ here first. These are the things that have already cost time.
67
67
  - **Hook failures are logged with a `[thatch]` prefix and never swallowed.** Two
68
68
  hooks were dead for weeks before failures were made visible. If you add a
69
69
  hook, log on failure.
70
- - **`chat.message` has two priority tiers**: extraction nudge first (returns
71
- early), then the prompt-aware recall nudge. Don't run both in one turn.
72
- - **The extraction nudge peeks, never flushes.** The buffer is NOT drained
73
- on nudge delivery (`extraction.peek()` in `triggerExtraction`, src/runtime.ts). It persists until the
74
- agent writes a memory or calls `thatch_extraction_done`. Ignored nudges
75
- accumulate; the `missedNudges` counter escalates the tone (polite at 0-1
76
- misses, insistent at 2, ALL-CAPS at 3+). The counter resets when the
77
- buffer drains. The primary opencode path doesn't use the nudge at all:
78
- `triggerExtraction` creates a child session directly, and the `extracting`
79
- set suppresses the nudge in `chat.message` while a direct-extraction child
80
- is active. The peek-not-drain semantics still hold for the fallback nudge
81
- path (MCP hosts and any `triggerExtraction` failure).
82
- - **A child sub-agent's `thatch_memory_remember` drains the parent's buffer**
83
- via the `childToParent` Map in src/runtime.ts (declaration plus `.get()`
84
- lookups in the idle and remember handlers). Two paths reach this machinery: (a) the **plugin-initiated child
85
- session** — `triggerExtraction` calls `client.session.create` with a
86
- `parentID`, the primary opencode path; (b) the **agent-initiated background
87
- task** — the model dispatches the fact-extractor via the `task` tool after
88
- receiving the nudge, the fallback path still used by MCP hosts. Both use
89
- the same `childToParent`/`parentSnapshots`/`consumeSnapshot` plumbing.
90
- Without this, either path would write memories in the child but never
91
- clear the parent's queue — the nudge would replay every turn.
92
- `thatch_extraction_done` is the belt-and-suspenders explicit
93
- acknowledgment: it drains the buffer without requiring a memory write
94
- (covers cases where the sub-agent errors out or the host doesn't expose
95
- parent-child session relationships).
70
+ - **There is no model-facing extraction nudge on opencode.** The old
71
+ `chat.message` extraction nudge (and its `missedNudges` escalation) drove
72
+ a model-driven handshake that raced its own state machine: parent acks
73
+ landed before the child's payload fetch, no-claim completions wiped
74
+ in-flight accepted sets, and mis-targeted session ids made every
75
+ transition a silent no-op - so the same buffered count re-fired forever
76
+ (the September 2026 dispatch-loop report). Extraction is now plugin-driven
77
+ end-to-end: `triggerExtraction` at session idle creates the child and
78
+ interpolates the session ID itself; the child's payload fetch records the
79
+ claim; completions are claim-scoped; the 15-minute stale reaper bounds
80
+ crashed extractors. The nudge survives on MCP hosts only, where no plugin
81
+ can create sessions and the file-backed queue is durable until the
82
+ extractor completes.
83
+ - **A child's `thatch_memory_remember` completes only its claimed delivery**
84
+ via the `childToParent` Map and the claim recorded by its payload fetch.
85
+ A child that never fetched has no claim, and its memory write (or idle
86
+ signal, or ack) consumes nothing - the entries stay held/pending for the
87
+ real extractor. `thatch_extraction_done` is the explicit no-save
88
+ completion: claim-scoped like everything else, so a mis-ordered or
89
+ mis-targeted ack can never drop another extractor's in-flight delivery.
96
90
  - **The no-save drain runs in the child-idle handler regardless of writes.**
97
91
  When the extraction child goes idle after a no-save run (nothing worth
98
92
  extracting), the parent's snapshot entries must still be drained from the
@@ -286,3 +280,16 @@ instead of throwing "Cannot use a closed database". Two rules keep this intact:
286
280
  wrap-up arming write, whose command executes are not drained by the v2
287
281
  cleanup). Do not "fix" the guard away by moving it into db.ts: the
288
282
  accessor cannot tell a legitimate in-flight caller from a stale writer.
283
+
284
+ ## Command frontmatter descriptions must be quoted YAML scalars
285
+
286
+ - **A plain-scalar frontmatter value containing a colon followed by a space
287
+ is invalid YAML, and not every host path rescues it.** `/thatch/hygiene`'s
288
+ description ("Tend the memory store: stale entries, ...") shipped unquoted;
289
+ the first parse throws
290
+ and only opencode's fallback sanitizer recovers it — and a long-lived
291
+ server was observed dropping the description entirely while a fresh server
292
+ parsed it fine, which made the bug look like a TUI rendering issue. The
293
+ renderer quotes every description (`yamlQuote` in src/commands.ts) and a
294
+ unit test pins the quoted form. Keep it that way for any new frontmatter
295
+ field whose value is free-form prose.
@@ -23,7 +23,7 @@ This document maps the feature parity and documents the remaining gaps.
23
23
  | **Session-start reminder** (recall nudge + hygiene heartbeat) | `session.created` event → `client.session.prompt` injects a synthetic message | `SessionStart` hook → `thatch reminder`; stdout becomes context | `sessionStart` hook → `thatch reminder --json`; output is `additional_context` JSON | **Full** |
24
24
  | **Compaction context** (re-familiarize after compaction) | `experimental.session.compacting` hook appends context to the compaction output | `PostCompact` hook — side-effects only, **cannot inject context** | No equivalent hook | **Gapped** — see below |
25
25
  | **Wrap-up commands** (`/thatch/compact`, `/thatch/exit`) | Command markdown synced to `$XDG_CONFIG_HOME/opencode/command/thatch/` at plugin init; `command.execute.before` arms the session and the next idle resolves the greenlight token into a TUI action (`session.compact` via `executeCommand`, `app.exit` via `publish`) | No equivalent — no plugin host to arm the check or trigger the action | No equivalent | **Gapped** — needs the plugin's SDK client and TUI control routes |
26
- | **On-demand actions** (`/thatch/defrag`, `extract`, `hygiene`, `reflect`) | Command markdown synced at plugin init; bodies rendered from the shared prompt cores with `thatch_` tool spellings; `extract` is opencode-only (needs session identity via `get_session_info` and the plugin's payload provider) | Command markdown synced by `thatch setup --claude` into `<claudeDir>/commands/thatch/`; same cores with `mcp__thatch__` spellings; `extract` excluded | MCP Prompts (`prompts/list` + `prompts/get`) from the same cores; Cursor surfaces them as slash commands; `extract` excluded | **Full** for the shared actions (same cores, per-host tool spellings); `extract` opencode-only |
26
+ | **On-demand actions** (`/thatch/defrag`, `extract`, `hygiene`, `reflect`, `refine`) | Command markdown synced at plugin init; bodies rendered from the shared prompt cores with `thatch_` tool spellings; `extract` is opencode-only (needs session identity via `get_session_info` and the plugin's payload provider) | Command markdown synced by `thatch setup --claude` into `<claudeDir>/commands/thatch/`; same cores with `mcp__thatch__` spellings; `extract` excluded | MCP Prompts (`prompts/list` + `prompts/get`) from the same cores; Cursor surfaces them as slash commands; `extract` excluded | **Full** for the shared actions (same cores, per-host tool spellings); `extract` opencode-only |
27
27
  | **Extraction** (buffer tool calls, extract durable facts) | `tool.execute.after` buffers non-meta tool calls in-process (`isMetaToolName` in `src/extraction.ts`: thatch_*, skill, task, agent, subagent — execute calls unwrapping only thatch tools are also excluded); **direct extraction** is the primary path — when the parent session goes idle with pending buffer entries, the plugin creates a child session via the SDK (`client.session.create` with `parentID`) and prompts it to call `get_extraction_payload` (nudge suppressed via the `extracting` set); the `chat.message` nudge path is a **fallback** used when direct extraction throws | `PostToolBatch` → `thatch buffer-batch` (file-backed JSONL queue); `UserPromptSubmit` → `thatch flush-tools` peeks the queue and prints the nudge with session ID + fetch tool name (does not drain — the queue persists until a memory write or `extraction_done`) | `postToolUse` → `thatch buffer-tool` (single-tool, file-backed queue); `beforeSubmitPrompt` → `thatch flush-tools --json` | **Partial** — opencode uses direct extraction (parent idle → child session) as the primary path, with the nudge as fallback; MCP hosts are nudge-only. Both paths use `get_extraction_payload` to fetch the queued interactions as a tool response, keeping the full payload out of the main session's context window. Nudge timing differs: the nudge arrives at the **start** of the next turn in Claude Code/Cursor; opencode's direct extraction fires at **end of turn** (session idle), and the nudge fallback fires at the start of the next turn like MCP |
28
28
  | **Extraction feedback** (toast notifications) | `client.tui.showToast` fires when the extraction child goes idle — shows `[thatch] new: N, updated: M, deleted: K` (success) or `extraction complete — nothing to save` (info); best-effort, silently ignored in headless mode | No TUI connection — MCP hosts have no equivalent | No TUI connection | **Gapped** — MCP hosts cannot show toast notifications |
29
29
  | **Automatic alerts** (notify on LLM pause / turn end) | Plugin event hook drives the `alerts.ts` state machine: `question.asked`/`permission.asked` → pause alert; `session.status` idle with a real-work round → done alert; unrecovered errors → needs-attention. Delivered via the shared `notify.ts` dispatcher, channel per event from the `alerts` config section | No event stream — only `thatch_notify_user` when the agent chooses | No event stream — only `thatch_notify_user` when the agent chooses | **Gapped** — MCP hosts have no plugin event hook to watch; Claude Code's `Notification`/`Stop` hooks could feed the same classifier later |
@@ -78,28 +78,27 @@ and the nudge (fallback):
78
78
  2. When the parent session goes idle (`session.status` idle event) with pending
79
79
  buffer entries, the plugin calls `triggerExtraction`: creates a child session
80
80
  via `client.session.create` (with `parentID`), then prompts it via
81
- `client.session.promptAsync` (or `prompt`) with the extraction payload. The
82
- `extracting` set suppresses the nudge in `chat.message` while the child runs.
81
+ `client.session.promptAsync` (or `prompt`) with the extraction prompt (the
82
+ parent's session ID is interpolated into the prompt by the plugin). The
83
+ `extracting` set gates re-triggering while the child runs.
83
84
  3. The child session runs the `thatch-fact-extractor` skill, writes memories via
84
- `thatch_memory_remember`. A memory write in the child drains the parent's
85
- buffer snapshot (via the `childToParent` Map and `consumeSnapshot`) and
86
- resets the missed-nudge counter.
87
- 4. When the child goes idle, the handler drains any remaining snapshot entries
88
- (covers no-save runs), deletes the child session, and fires a toast via
85
+ `thatch_memory_remember`. The child's payload fetch records its claim; its
86
+ memory write (or `extraction_done`) completes that claim - consuming only
87
+ the entries it received.
88
+ 4. When the child goes idle, the handler finalizes the claim-scoped
89
+ completion, deletes the child session, and fires a toast via
89
90
  `client.tui.showToast`.
90
91
 
91
- **Nudge (fallback path):**
92
+ **Failure recovery (no model-facing nudge exists on opencode):**
92
93
 
93
94
  1. If `triggerExtraction` throws (session creation or prompt fails), the catch
94
- block clears the `extracting` flag. On the next `chat.message`, the buffer is
95
- **peeked** (not drained) and a synthetic text part carrying the JSON payload
96
- is injected into the conversation. The buffer persists until the agent writes
97
- a memory or calls `thatch_extraction_done`, so ignored nudges repeat and
98
- escalate (polite → insistent → ALL-CAPS) via the `missedNudges` counter.
99
- 2. The agent sees the nudge, loads the `thatch-fact-extractor` skill, and saves
100
- durable facts via `thatch_memory_remember`. A memory write in the session
101
- (or a child sub-agent via the `childToParent` Map) or `thatch_extraction_done`
102
- drains the buffer and resets the missed-nudge counter.
95
+ block clears the `extracting` flag. The pending buffer is untouched, and
96
+ the next idle re-triggers extraction - the plugin retries without any
97
+ model cooperation.
98
+ 2. If the child errors or is deleted before completing, what it held is
99
+ requeued to pending. If its completion signal never comes at all, the
100
+ 15-minute stale reaper returns accepted entries to pending. Either way
101
+ the next idle re-extracts - no interaction is silently lost.
103
102
 
104
103
  Claude Code and Cursor use a **file-backed** queue (`src/extract-queue.ts`)
105
104
  because hooks fire one-shot with no cross-call state:
@@ -25,7 +25,7 @@ code (`src/opencode/v1.ts`):
25
25
  | `experimental.session.compacting` | `{ sessionID }` | `{ context: string[] }` | Marks the session as compacting, pushes re-familiarization context. |
26
26
  | `experimental.compaction.autocontinue` | `{ sessionID }` | `{ enabled: boolean }` | Clears the compacting flag so `chat.message` nudges resume post-compaction. |
27
27
  | `tool.execute.after` | `{ tool, sessionID, callID, args }` | `{ title, output, metadata }` | Buffers non-`thatch_`, non-`skill`, non-`task` tool calls into the in-memory extraction ring buffer (max 20). In child sessions, also tracks new/updated/deleted counts via `childMetrics` for the extraction toast. |
28
- | `chat.message` | `{ sessionID, messageID }` | `{ message, parts }` | Two tiers: extraction nudge if pending and not already extracting (fallback — direct extraction via the `event` hook is the primary path), else prompt-aware recall nudge. Suppressed while compacting, while `extracting` is set, or for task-dispatched sub-agent sessions (children that are not extraction children). |
28
+ | `chat.message` | `{ sessionID, messageID }` | `{ message, parts }` | Prompt-aware recall nudge (plus prediction/behavior auto-fire). No extraction nudge - extraction is plugin-driven at session idle. Suppressed while compacting or for task-dispatched sub-agent sessions (children that are not extraction children). |
29
29
  | `event` | `{ event: { type, properties } }` | — | Dispatches on `event.type`: `session.created` (top-level) → `client.session.prompt` with reminder + hygiene heartbeat; `session.created` (child with `parentID`) → records `childToParent` + snapshots parent's pending buffer. `session.status` idle (parent, pending interactions) → `triggerExtraction` (direct child-session extraction); idle (child) → complete/drain, `showToast` with metrics, delete child. `session.error` (child) → requeue parent's accepted entries. `session.deleted` → requeue if child, drop accepted if parent, clean maps. `session.compacted` → clear compacting flag. |
30
30
  | `dispose` | — | — | Closes the DB. |
31
31
 
@@ -55,7 +55,14 @@ description: Extract durable project facts ... Use when ...
55
55
  | `thatch-code-walkthrough` | Explain a feature, module, or workflow to the user as a teaching walkthrough: identify the code area (optionally from a branch or PR), research how it works, teach it with file:line citations and analogies, list the key files. |
56
56
  | `thatch-session-reflection` | End-of-session memory recording (project, user, tools, self). |
57
57
  | `thatch-coding-workflow` | Plan and execute code changes with a task-list-driven workflow. Use when implementing features, fixing bugs, or making multi-file changes. |
58
- | `thatch-plan-refinement` | Refine a plan before implementing it: fresh-context reviewer rounds until consensus, with an optional deep-mode lens fan-out (reuse, alternatives, hidden problems, safe-to-modify, archaeology). |
58
+ | `thatch-plan-refinement` | Refine a plan before implementing it: fresh-context reviewer rounds until consensus, with an optional deep-mode lens fan-out (reuse, alternatives, hidden problems, safe-to-modify, archaeology). The generic core loop. |
59
+ | `thatch-refine` | Classify the project (audience, lifecycle, surface) and refine a plan under that type's requirements. Entry point for the `thatch-refine-*` family; also the `/thatch/refine` command. |
60
+ | `thatch-refine-team-app` | Refinement deltas for multi-developer team codebases: consistency outranks cleverness, intent breadcrumbs are deliverables, pre-mortem and safe-to-modify lenses dominate. |
61
+ | `thatch-refine-personal` | Refinement deltas for solo projects with no downstream consumers: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest. |
62
+ | `thatch-refine-shared-lib` | Refinement deltas for changes touching a library's API surface: flexible on input, strict on output, keep special cases out of the API, never foreclose caller options, docs are a deliverable. |
63
+ | `thatch-refine-spike` | Refinement deltas for throwaway prototypes: skip the loop, verify only the premises the experiment's conclusion depends on, state the question and the time box. |
64
+ | `thatch-refine-infra` | Refinement deltas for CI, Terraform, and k8s changes: blast radius across pipelines and environments, rollback plan and staged verification are mandatory, pattern-following is near-absolute. |
65
+ | `thatch-refine-oss-contribution` | Refinement deltas for PRs to repos you do not maintain: the maintainer's design authority governs, minimal diff, upstream discussion gates significant design work. |
59
66
  | `thatch-pr-description` | Draft PR descriptions with SYNOPSIS / PURPOSE / DESCRIPTION / WALK-THROUGH / NOTES, project-context research, clarity checks, and bold+italic emphasis for scanning. |
60
67
  | `thatch-ticket-description` | Draft ticket/issue descriptions with clear sections, project-context research, clarity checks, and bold+italic emphasis for scanning. |
61
68
  | `thatch-split-overlarge-pr` | Split already-completed work from an overlarge PR into human-reviewable, release-safe PRs targeting main. |
@@ -328,6 +328,13 @@ response, walkthrough, and workflow skills that support the review pipeline:
328
328
  | `thatch-code-walkthrough` | Explains a feature, module, or workflow to the user as a teaching walkthrough with file:line citations. Also used to draft high-level docs for new or undocumented features. |
329
329
  | `thatch-coding-workflow` | Plans and executes code changes with a task-list-driven workflow: complexity triage, milestone planning, research before coding, post-coding verification. Pairs with `thatch-code-archaeology` (research first, then plan). |
330
330
  | `thatch-plan-refinement` | Refines a plan before it is implemented: a fresh-context reviewer subagent re-checks the plan each round until consensus; deep mode adds specialist lenses (reuse, alternatives, hidden problems, safe-to-modify, intent archaeology). |
331
+ | `thatch-refine` | Classifies the project (audience, lifecycle, surface) and refines a plan under that type's requirements. The entry point for the `thatch-refine-*` family; also the `/thatch/refine` command. |
332
+ | `thatch-refine-team-app` | Team codebases: consistency outranks cleverness, intent breadcrumbs are deliverables, pre-mortem and safe-to-modify lenses dominate. |
333
+ | `thatch-refine-personal` | Solo projects: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest. |
334
+ | `thatch-refine-shared-lib` | Library API surfaces: flexible on input, strict on output, keep special cases out of the API, never foreclose caller options, docs are a deliverable. |
335
+ | `thatch-refine-spike` | Throwaway prototypes: skip the loop, verify only the premises the experiment's conclusion depends on. |
336
+ | `thatch-refine-infra` | CI, Terraform, and k8s changes: blast radius, mandatory rollback plan and staged verification, pattern-following is near-absolute. |
337
+ | `thatch-refine-oss-contribution` | PRs to repos you do not maintain: the maintainer's design authority governs, minimal diff, upstream discussion gates significant design work. |
331
338
 
332
339
  ### Writing skills
333
340
 
@@ -17,6 +17,13 @@ or you invoke it.
17
17
  The agent asks before forgetting anything that may still matter.
18
18
  - `/thatch/reflect` — run the session-reflection skill to persist what the
19
19
  current session learned.
20
+ - `/thatch/refine` — refine a plan for a code change under the current
21
+ project type's requirements: the agent classifies the project (team,
22
+ personal, shared library, spike, infrastructure, or open source
23
+ contribution), confirms the pick, and runs the refinement loop with
24
+ that type's expectations applied. Text after the command can name the
25
+ type directly (for example, `/thatch/refine shared-lib`) to skip
26
+ classification.
20
27
  - `/thatch/compact` and `/thatch/exit` — the wrap-up commands. They run a
21
28
  pre-flight checklist (flush extraction, finish promised memory writes,
22
29
  surface loose ends) before a compaction or an exit, gated by a greenlight
@@ -31,6 +38,7 @@ or you invoke it.
31
38
  | `/thatch/extract` | yes | no | no |
32
39
  | `/thatch/hygiene` | yes | yes | yes (as an MCP prompt) |
33
40
  | `/thatch/reflect` | yes | yes | yes (as an MCP prompt) |
41
+ | `/thatch/refine` | yes | yes | yes (as an MCP prompt) |
34
42
  | `/thatch/compact`, `/thatch/exit` | yes | no | no |
35
43
 
36
44
  Why the gaps: `/thatch/extract` needs the invoking session's ID, which only
@@ -77,6 +77,18 @@ Skills are split into shared (all hosts) and opencode-only:
77
77
  | `thatch-coding-workflow` | Plans and executes code changes with a task-list-driven workflow: complexity triage, milestone planning, research before coding, post-coding verification. Pairs with `thatch-code-archaeology` (research first, then plan). |
78
78
  | `thatch-plan-refinement` | Refines a plan before it is implemented: a fresh-context reviewer subagent re-checks the plan each round until consensus; deep mode adds specialist lenses (reuse, alternatives, hidden problems, safe-to-modify, intent archaeology). |
79
79
 
80
+ ### Plan refinement skills
81
+
82
+ | Skill | Purpose |
83
+ |-------|---------|
84
+ | `thatch-refine` | Classifies the project (audience, lifecycle, surface) and refines a plan under that type's requirements. The entry point for the `thatch-refine-*` family; also the `/thatch/refine` command. |
85
+ | `thatch-refine-team-app` | Team codebases: consistency outranks cleverness, intent breadcrumbs are deliverables, pre-mortem and safe-to-modify lenses dominate. |
86
+ | `thatch-refine-personal` | Solo projects: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest. |
87
+ | `thatch-refine-shared-lib` | Library API surfaces: flexible on input, strict on output, keep special cases out of the API, never foreclose caller options, docs are a deliverable. |
88
+ | `thatch-refine-spike` | Throwaway prototypes: skip the loop, verify only the premises the experiment's conclusion depends on. |
89
+ | `thatch-refine-infra` | CI, Terraform, and k8s changes: blast radius, mandatory rollback plan and staged verification, pattern-following is near-absolute. |
90
+ | `thatch-refine-oss-contribution` | PRs to repos you do not maintain: the maintainer's design authority governs, minimal diff, upstream discussion gates significant design work. |
91
+
80
92
  ## Stale skill cleanup
81
93
 
82
94
  When skills are renamed or removed in a new release, thatch automatically
@@ -16,6 +16,12 @@ baseline, then polls the GitHub API through the `gh` CLI once a
16
16
  minute. When the target changes in a way the watcher cares about, the
17
17
  plugin prompts the agent with a notification.
18
18
 
19
+ Register before you push. CI notifications fire when a check run goes
20
+ from running to completed, so a watcher created after a fast pipeline
21
+ has already finished will never fire for that commit. If every check on
22
+ the head is already done when the watcher is created, the response says
23
+ so and reports the results directly.
24
+
19
25
  PR watched events:
20
26
 
21
27
  - `pr_comment` - new top-level comments on the PR
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",
package/src/chat.ts CHANGED
@@ -212,7 +212,7 @@ export type ChatHostKind = "opencode" | "mcp";
212
212
  // Display names appear inside wake prompts and chat_list output, so they are
213
213
  // capped tight. Bodies are capped so a runaway sender cannot turn a nudge
214
214
  // into a context bomb; the reader fetches full content via chat_read anyway.
215
- const MAX_BODY_LEN = 10_000;
215
+ export const MAX_BODY_LEN = 10_000;
216
216
 
217
217
  // Assigned names are lowercase slugs of a pool name with a numeric counter
218
218
  // suffix ("al-go-rithm-00001"), which satisfies NAME_CHARSET. The charset
package/src/commands.ts CHANGED
@@ -100,6 +100,16 @@ After dispatching, call thatch_extraction_done to acknowledge.`,
100
100
  description: "Tend the memory store: stale entries, orphans, pending dedup pairs",
101
101
  body: hygieneCore(),
102
102
  },
103
+ {
104
+ name: "refine",
105
+ description: "Refine a plan under the project type's requirements (classifies the project first)",
106
+ // A skill trigger, not a nudge behavior: the body points at the
107
+ // thatch-refine router skill, which classifies the project and loads
108
+ // the matching type deltas on top of the plan-refinement core loop.
109
+ body: `Load and follow the thatch-refine skill to refine a plan for the current project.
110
+
111
+ The user message may name a project type (team-app, personal, shared-lib, spike, infra, oss-contribution); if it does, skip classification and use that type. If no plan is drafted yet in this session, the skill's loop starts with drafting one.`,
112
+ },
103
113
  {
104
114
  name: "reflect",
105
115
  description: "Persist what this session learned as memories",
@@ -108,10 +118,20 @@ After dispatching, call thatch_extraction_done to acknowledge.`,
108
118
  ];
109
119
  }
110
120
 
121
+ /**
122
+ * Renders a YAML double-quoted scalar. Plain scalars break on values
123
+ * containing ": " (invalid YAML - the mapping key would be ambiguous), and
124
+ * opencode's lenient fallback parser does not rescue every host path, so the
125
+ * description must always be quoted.
126
+ */
127
+ function yamlQuote(value: string): string {
128
+ return JSON.stringify(value);
129
+ }
130
+
111
131
  /** Renders one action as a full command markdown file. */
112
132
  export function renderActionCommand(action: ActionDef): string {
113
133
  return `---
114
- description: ${action.description}
134
+ description: ${yamlQuote(action.description)}
115
135
  ---
116
136
  ${userMessageSection}
117
137
 
@@ -121,7 +141,7 @@ ${action.body}`;
121
141
  }
122
142
 
123
143
  const COMPACT_TEMPLATE = `---
124
- description: Flush thatch persistence, check for loose ends, then compact if clear
144
+ description: ${yamlQuote("Flush thatch persistence, check for loose ends, then compact if clear")}
125
145
  ---
126
146
  ${userMessageSection}
127
147
 
@@ -138,7 +158,7 @@ ${COMPACT_READY_TOKEN}
138
158
  If anything is outstanding, list the items concisely so the user can address them, and do NOT include the token. The session will only be compacted when the token is present.`;
139
159
 
140
160
  const EXIT_TEMPLATE = `---
141
- description: Flush thatch persistence, check for loose ends, then exit opencode if clear
161
+ description: ${yamlQuote("Flush thatch persistence, check for loose ends, then exit opencode if clear")}
142
162
  ---
143
163
  ${userMessageSection}
144
164
 
@@ -56,10 +56,25 @@ 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" || lower === "mcp__thatch__extraction_done") {
59
+ if (lower === "mcp__thatch__memory_remember") {
60
+ // The session that writes a memory has processed its queued
61
+ // interactions itself - drain. This is the parent's self-serve path.
60
62
  resetMissedCount(sessionID);
61
63
  consumeQueue(sessionID);
62
64
  existing.length = 0;
65
+ } else if (lower === "mcp__thatch__extraction_done") {
66
+ // An extraction_done ack must NOT drain the queue. When the PARENT
67
+ // acks after dispatching (per the nudge), a drain here deletes the
68
+ // queue before the sub-agent's get_extraction_payload fetch - the
69
+ // accept-before-fetch race, which on this path is a hard delete
70
+ // (silent interaction loss). The queue is consumed instead by the
71
+ // extractor's completion: extraction_done with the parent's
72
+ // session_id (drainExtractionQueue in mcp.ts), or the parent's own
73
+ // memory_remember above. A no-drain ack only resets the escalation
74
+ // counter - the queue persists on disk (durable across
75
+ // interruption), and an extractor that dies without completing
76
+ // leaves it in place for the next nudge to re-dispatch.
77
+ resetMissedCount(sessionID);
63
78
  }
64
79
  if (lower.startsWith("mcp__thatch__")) continue;
65
80
  if (lower === "skill" || lower === "task" || lower === "agent") continue;
package/src/extraction.ts CHANGED
@@ -342,9 +342,10 @@ export class ExtractionPipeline {
342
342
  * Requeues accepted entries that have waited longer than maxAgeMs for a
343
343
  * completion signal that never came: the extractor sub-agent crashed
344
344
  * without acking, or the completion ack was lost. Without this, accepted
345
- * entries linger unprocessed AND un-nudged - silent loss. Returns the session ids that had entries
346
- * requeued; the regular nudge path picks them up from pending on the
347
- * session's next message.
345
+ * entries linger unprocessed with nothing to re-extract them - silent
346
+ * loss. Returns the session ids that had entries requeued; on opencode
347
+ * the idle trigger re-extracts them, on MCP hosts the next flush-tools
348
+ * nudge picks them up.
348
349
  */
349
350
  requeueStaleAccepted(maxAgeMs = ACCEPTED_STALE_MS): string[] {
350
351
  const stale = [...this.#acceptedAt.entries()]
@@ -529,8 +529,9 @@ function buildCapabilities(context: V2Context, worktree: string, childSessions:
529
529
  // returns the created SessionInfo directly.
530
530
  const result = await session.create({ location: { directory: worktree }, title: input.title });
531
531
  const id = result?.id;
532
- // A shape mismatch must fail into the runtime's extraction fallback
533
- // (the nudge path), not silently corrupt the child-session maps.
532
+ // A shape mismatch must fail loudly: the runtime logs it and retries
533
+ // extraction at the next idle. It must not silently corrupt the
534
+ // child-session maps.
534
535
  if (!id) throw new Error(`v2 session.create returned no id: ${JSON.stringify(result)?.slice(0, 200)}`);
535
536
  // Register for event forwarding: the child's events carry the project
536
537
  // directory, which the pump's directory filter would otherwise drop.
@@ -539,7 +540,7 @@ function buildCapabilities(context: V2Context, worktree: string, childSessions:
539
540
  },
540
541
  // No delete on the v2 SessionDomain: extraction child sessions are not
541
542
  // cleaned up on v2 (documented gap; the bookkeeping maps still keep the
542
- // nudge path consistent). Two user-visible consequences: the session
543
+ // claim/completion lifecycle consistent). Two user-visible consequences: the session
543
544
  // picker accumulates one thatch-extraction entry per extraction, and
544
545
  // "continue last session" (-c) logic that picks the newest top-level
545
546
  // session will land in an extraction child after any session that
package/src/prompts.ts CHANGED
@@ -192,6 +192,13 @@ workflows. The host auto-discovers them, but reach for them proactively:
192
192
  - \`thatch-review-followup\` - alternate entrypoint for follow-up review rounds. Verifies whether the author's responses and code changes since your last review adequately addressed prior findings, offers to reply on resolved items, then optionally re-runs the full structured review.
193
193
  - \`thatch-code-archaeology\` - investigate an existing feature, debug an unfamiliar area, or begin a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features) before proposing changes. The research skill; pair with \`thatch-coding-workflow\`.
194
194
  - \`thatch-plan-refinement\` - refine a plan before implementing it: fresh-context reviewer rounds until consensus, with an optional deep-mode lens fan-out (reuse, alternatives, hidden problems, safe-to-modify, archaeology). Use when a plan for a code change is about to be built or the user asks to stress-test a plan.
195
+ - \`thatch-refine\` - classify the project (audience, lifecycle, surface) and refine a plan under that type's requirements: team codebases owe coordination and intent breadcrumbs, personal projects allow cleverness, shared libraries owe API hygiene. Entry point for the thatch-refine-* family; also the /thatch/refine command.
196
+ - \`thatch-refine-team-app\` - refinement deltas for multi-developer team codebases: consistency outranks cleverness, intent breadcrumbs are deliverables, pre-mortem and safe-to-modify lenses dominate.
197
+ - \`thatch-refine-personal\` - refinement deltas for solo projects with no downstream consumers: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest.
198
+ - \`thatch-refine-shared-lib\` - refinement deltas for changes touching a library's API surface: flexible on input, strict on output, keep special cases out of the API, never foreclose caller options, docs are a deliverable.
199
+ - \`thatch-refine-spike\` - refinement deltas for throwaway prototypes: skip the loop, verify only the premises the experiment's conclusion depends on, state the question and the time box.
200
+ - \`thatch-refine-infra\` - refinement deltas for CI, Terraform, and k8s changes: blast radius across pipelines and environments, rollback plan and staged verification are mandatory, pattern-following is near-absolute.
201
+ - \`thatch-refine-oss-contribution\` - refinement deltas for PRs to repos you do not maintain: the maintainer's design authority governs, minimal diff, upstream discussion gates significant design work.
195
202
  - \`thatch-change-walkthrough\` - produces a teaching walkthrough of a diff with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + change overlay mirroring the same numbers) and calibrated prose rules. Load the skill for the format and prose rules, not just for the research method.
196
203
  - \`thatch-code-walkthrough\` - produces a teaching walkthrough for a feature/workflow as it stands today with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + Key files) and calibrated prose rules. Also use proactively to draft high-level docs for new or undocumented features (suitable for pkg/README.md or docs/features/<name>.md). Load the skill for the format and prose rules, not just for the research method.
197
204
  - \`thatch-review-context\` - gather project context (PRs, tickets, TODOs, deferred work) before a review.
@@ -506,6 +513,13 @@ workflows. The host auto-discovers them, but reach for them proactively:
506
513
 
507
514
  - \`thatch-code-archaeology\` - investigate an existing feature, debug an unfamiliar area, or begin a new ticket. Explores the code base from multiple angles (data model, state flow, git history, sibling features) before proposing changes. The research skill; pair with \`thatch-coding-workflow\`.
508
515
  - \`thatch-plan-refinement\` - refine a plan before implementing it: fresh-context reviewer rounds until consensus, with an optional deep-mode lens fan-out (reuse, alternatives, hidden problems, safe-to-modify, archaeology). Use when a plan for a code change is about to be built or the user asks to stress-test a plan.
516
+ - \`thatch-refine\` - classify the project (audience, lifecycle, surface) and refine a plan under that type's requirements: team codebases owe coordination and intent breadcrumbs, personal projects allow cleverness, shared libraries owe API hygiene. Entry point for the thatch-refine-* family; also the /thatch/refine command.
517
+ - \`thatch-refine-team-app\` - refinement deltas for multi-developer team codebases: consistency outranks cleverness, intent breadcrumbs are deliverables, pre-mortem and safe-to-modify lenses dominate.
518
+ - \`thatch-refine-personal\` - refinement deltas for solo projects with no downstream consumers: relaxed coordination, cleverness and experimentation welcome, premise verification stays honest.
519
+ - \`thatch-refine-shared-lib\` - refinement deltas for changes touching a library's API surface: flexible on input, strict on output, keep special cases out of the API, never foreclose caller options, docs are a deliverable.
520
+ - \`thatch-refine-spike\` - refinement deltas for throwaway prototypes: skip the loop, verify only the premises the experiment's conclusion depends on, state the question and the time box.
521
+ - \`thatch-refine-infra\` - refinement deltas for CI, Terraform, and k8s changes: blast radius across pipelines and environments, rollback plan and staged verification are mandatory, pattern-following is near-absolute.
522
+ - \`thatch-refine-oss-contribution\` - refinement deltas for PRs to repos you do not maintain: the maintainer's design authority governs, minimal diff, upstream discussion gates significant design work.
509
523
  - \`thatch-change-walkthrough\` - produces a teaching walkthrough of a diff with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + change overlay mirroring the same numbers) and calibrated prose rules. Load the skill for the format and prose rules, not just for the research method.
510
524
  - \`thatch-code-walkthrough\` - produces a teaching walkthrough for a feature/workflow as it stands today with a specific format (SYNOPSIS + per-workflow orient/mechanism/numbered-stages + Key files) and calibrated prose rules. Also use proactively to draft high-level docs for new or undocumented features (suitable for pkg/README.md or docs/features/<name>.md). Load the skill for the format and prose rules, not just for the research method.
511
525
  - \`thatch-review-context\` - gather project context (PRs, tickets, TODOs, deferred work) before a review.
@@ -788,46 +802,25 @@ export function reflectCore(tool: ToolNamer): string {
788
802
  }
789
803
 
790
804
  /**
791
- * Extraction nudge with escalation. Both the opencode plugin (src/runtime.ts)
792
- * and the Claude Code/Cursor CLI (bin/thatch flush-tools) call this with
793
- * their respective tool name prefix. The missedCount parameter tracks
794
- * consecutive nudges delivered without any memory_remember call in between,
795
- * escalating the tone from polite to insistent to shouting.
805
+ * Extraction nudge with escalation - MCP hosts only (Claude Code / Cursor).
806
+ * There is deliberately no opencode variant: the opencode plugin extracts via
807
+ * its own child sessions (triggerExtraction at session idle) and no longer
808
+ * asks the model to run the handshake. Only hosts with no plugin lifecycle
809
+ * (each hook a fresh process, no session-creation API) still need a model-
810
+ * driven dispatch, and their file-backed queue (extract-queue.ts) is durable
811
+ * until the extractor's completion, so the handshake cannot lose entries
812
+ * there.
813
+ *
814
+ * The missedCount parameter tracks consecutive nudges delivered without any
815
+ * memory_remember call in between, escalating the tone from polite to
816
+ * insistent to shouting.
796
817
  *
797
818
  * The sessionID replaces the inline payload: the nudge tells the model to
798
819
  * dispatch a sub-agent that calls get_extraction_payload to fetch the queued
799
820
  * interactions as a tool response. This keeps the full payload out of the
800
821
  * main session's context window.
801
- *
802
- * Call extractionNudgeOpencode for the opencode plugin path (task tool with
803
- * background parameter) or extractionNudgeMcp for the Claude Code/Cursor
804
- * CLI path (generic sub-agent wording).
805
822
  */
806
823
  export function extractionNudge(
807
- count: number,
808
- missedCount: number,
809
- toolName: string,
810
- sessionID: string,
811
- ): string {
812
- const isOpencode = toolName.startsWith("thatch_");
813
- return isOpencode
814
- ? extractionNudgeOpencode(count, missedCount, sessionID)
815
- : extractionNudgeMcp(count, missedCount, sessionID);
816
- }
817
-
818
- function extractionNudgeOpencode(
819
- count: number,
820
- missedCount: number,
821
- sessionID: string,
822
- ): string {
823
- const plural = count === 1 ? "" : "s";
824
- const dispatch = 'Dispatch a task with background: true and subagent_type: "general" (required for thatch tool access)';
825
- const drainTool = "thatch_extraction_done";
826
- const fetchTool = "thatch_get_extraction_payload";
827
- return buildExtractionNudge(count, missedCount, plural, dispatch, drainTool, fetchTool, sessionID);
828
- }
829
-
830
- function extractionNudgeMcp(
831
824
  count: number,
832
825
  missedCount: number,
833
826
  sessionID: string,