@jeffober/thatch 0.1.11 → 0.1.13

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.
@@ -1,7 +1,7 @@
1
1
  # Skills
2
2
 
3
3
  Thatch installs SKILL.md files that give the agent task-specific workflows. All
4
- skill content is defined in `src/skills.ts` and installed to a host-specific
4
+ skill content lives in `artifacts/skills/*.md` and is loaded by `src/skills.ts` and installed to a host-specific
5
5
  directory. See `mcp-parity.md` for which skills each host gets.
6
6
 
7
7
  ## Skill file format
@@ -21,9 +21,9 @@ description: Extract durable project facts ... Use when ...
21
21
  - `description` drives when the agent loads the skill (both opencode and
22
22
  Cursor auto-discover skills and use the description for relevance).
23
23
 
24
- ## The 13 skills
24
+ ## The 14 skills
25
25
 
26
- **Shared (12)** — installed everywhere; no sub-agents required:
26
+ **Shared (13)** — installed everywhere; no sub-agents required:
27
27
 
28
28
  | Skill | Role |
29
29
  |-------|------|
@@ -35,6 +35,7 @@ description: Extract durable project facts ... Use when ...
35
35
  | `thatch-review-state-flow` | Data flow and contracts: module boundaries, implicit FSMs, error propagation. |
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
+ | `thatch-review-mark-and-sweep` | Mechanical change completeness: whole-repo sweep for stragglers after renames, flag removals, API substitutions. |
38
39
  | `thatch-review-synthesizer` | Verify specialist findings against code, dedupe, classify, calibrate severity. |
39
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. |
40
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. |
@@ -44,11 +45,11 @@ description: Extract durable project facts ... Use when ...
44
45
 
45
46
  | Skill | Role |
46
47
  |-------|------|
47
- | `thatch-code-review` | Resolve review target, gather project context, research affected workflows, estimate complexity, partition, dispatch the 5 specialists in parallel, synthesize. |
48
+ | `thatch-code-review` | Resolve review target, gather project context, research affected workflows, estimate complexity, partition, dispatch the 6 specialists in parallel, synthesize. |
48
49
 
49
50
  ## REVIEW_COMMON
50
51
 
51
- The five review specialists share a framework interpolated via `${REVIEW_COMMON}`:
52
+ The six review specialists share a framework interpolated via `${REVIEW_COMMON}`:
52
53
 
53
54
  - **Static analysis only** — no running tests/linters/compilers.
54
55
  - **Scope gathering** — resolve the git range, `git diff --stat`, read changed
@@ -78,7 +79,7 @@ The synthesizer reuses the same verification rigor but has its own structure
78
79
  ## The two arrays
79
80
 
80
81
  ```ts
81
- const SHARED_SKILLS: SkillDef[] = [ /* 12 skills above */ ];
82
+ const SHARED_SKILLS: SkillDef[] = [ /* 13 skills above */ ];
82
83
  const OPENCODE_ONLY_SKILLS: SkillDef[] = [ /* code-review coordinator */ ];
83
84
  ```
84
85
 
@@ -101,18 +102,15 @@ and `--cursor` pass only the shared set.
101
102
 
102
103
  ## Adding a skill
103
104
 
104
- 1. Write the `SKILL.md` content as a template literal in `src/skills.ts`.
105
- Interpolate `${REVIEW_COMMON}` if it's a review specialist.
106
- 2. Add a `{ name, content }` entry to `SHARED_SKILLS` (or `OPENCODE_ONLY_SKILLS`
107
- if it needs sub-agents).
108
- 3. If the skill name appears in a tool's expected workflow (e.g. the extraction
109
- nudge references `thatch-fact-extractor`), update that prompt in `src/prompts.ts`.
110
- 4. The opencode plugin and `thatch setup` pick it up automatically on next init.
105
+ 1. Write `artifacts/skills/thatch-<name>.md` with YAML frontmatter. If it's a review specialist, include `${REVIEW_COMMON}` (interpolated from `artifacts/skills/common.md`).
106
+ 2. Add the skill name to the `names` array in `loadSharedSkills()` (or `loadOpencodeOnlySkills()` if sub-agents are needed). The loader in `src/skills.ts` reads `.md` files at init.
107
+ 3. If the skill name appears in a tool's workflow (e.g. `thatch-fact-extractor`), update `src/prompts.ts`.
108
+ 4. Run `mise run check` — tests verify counts; `installSkills` picks up new files on next init.
111
109
 
112
110
  ## Memory review skills in practice
113
111
 
114
112
  - **Quick single lens**: load any specialist directly and point it at a branch.
115
- - **Full review on opencode**: load `thatch-code-review` — it gathers project context, researches affected workflows, dispatches all 5 specialists in parallel (with both the context brief and workflow guide injected into each briefing), then synthesizes.
113
+ - **Full review on opencode**: load `thatch-code-review` — it gathers project context, researches affected workflows, dispatches all 6 specialists in parallel (with both the context brief and workflow guide injected into each briefing), then synthesizes.
116
114
  - **Full review on Claude Code/Cursor**: run each specialist in sequence, then
117
115
  run `thatch-review-synthesizer` to verify and aggregate.
118
116
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffober/thatch",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
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",
@@ -15,7 +15,8 @@
15
15
  "files": [
16
16
  "src",
17
17
  "docs",
18
- "bin"
18
+ "bin",
19
+ "artifacts"
19
20
  ],
20
21
  "scripts": {
21
22
  "test": "bun test",
@@ -37,11 +37,16 @@ function queuePath(sessionID: string): string {
37
37
  }
38
38
 
39
39
  /**
40
- * Buffer a batch of tool calls for later extraction. Filters out thatch's own
41
- * MCP tools (mcp__thatch__*) to avoid self-echo — extracting facts from memory
42
- * operations would just write the store back into the store. Each surviving
43
- * call is mapped to a ToolInteraction (title synthesized since Claude Code
44
- * doesn't supply one) and appended as one JSON line.
40
+ * Buffer a batch of tool calls for later extraction. Filters out:
41
+ * - thatch's own MCP tools (mcp__thatch__*) to avoid self-echo — extracting
42
+ * facts from memory operations would just write the store back into itself.
43
+ * - skill/task/agent meta-tools — these orchestrate agent behavior (loading
44
+ * skills, dispatching sub-agents) and buffering them creates a feedback
45
+ * loop where the extraction nudge triggers a skill load, which gets
46
+ * buffered, which triggers another nudge on the next turn.
47
+ * Memory writes reset the missed-nudge escalation counter.
48
+ * Each surviving call is mapped to a ToolInteraction (title synthesized since
49
+ * Claude Code doesn't supply one) and appended as one JSON line.
45
50
  */
46
51
  export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void {
47
52
  const dir = queueDir();
@@ -50,7 +55,14 @@ export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void
50
55
  const existing = readQueueFile(path);
51
56
  const additions: ToolInteraction[] = [];
52
57
  for (const tc of toolCalls) {
53
- if (tc.tool_name.startsWith("mcp__thatch__")) continue;
58
+ const lower = tc.tool_name.toLowerCase();
59
+ if (lower === "mcp__thatch__memory_remember") {
60
+ resetMissedCount(sessionID);
61
+ consumeQueue(sessionID);
62
+ existing.length = 0;
63
+ }
64
+ if (lower.startsWith("mcp__thatch__")) continue;
65
+ if (lower === "skill" || lower === "task" || lower === "agent") continue;
54
66
  const response = typeof tc.tool_response === "string"
55
67
  ? tc.tool_response
56
68
  : JSON.stringify(tc.tool_response).slice(0, 500);
@@ -70,8 +82,8 @@ export function appendBatch(sessionID: string, toolCalls: BatchToolCall[]): void
70
82
  writeFileSync(path, all.map((ix) => JSON.stringify(ix)).join("\n") + "\n");
71
83
  }
72
84
 
73
- /** Read and delete a session's queue. Returns the buffered interactions in order. */
74
- export function flushQueue(sessionID: string): ToolInteraction[] {
85
+ /** Read a session's queued interactions without removing them. */
86
+ export function peekQueue(sessionID: string): ToolInteraction[] {
75
87
  let path: string;
76
88
  try {
77
89
  path = queuePath(sessionID);
@@ -79,8 +91,18 @@ export function flushQueue(sessionID: string): ToolInteraction[] {
79
91
  return [];
80
92
  }
81
93
  if (!existsSync(path)) return [];
82
- const interactions = readQueueFile(path);
83
- unlinkSync(path);
94
+ return readQueueFile(path);
95
+ }
96
+
97
+ /** Delete a session's queue file. Called when the agent writes a memory. */
98
+ export function consumeQueue(sessionID: string): void {
99
+ try { unlinkSync(queuePath(sessionID)); } catch { /* not present is fine */ }
100
+ }
101
+
102
+ /** Read and delete a session's queue. Returns the buffered interactions in order. */
103
+ export function flushQueue(sessionID: string): ToolInteraction[] {
104
+ const interactions = peekQueue(sessionID);
105
+ consumeQueue(sessionID);
84
106
  return interactions;
85
107
  }
86
108
 
@@ -98,4 +120,33 @@ function readQueueFile(path: string): ToolInteraction[] {
98
120
  }
99
121
  })
100
122
  .filter((ix): ix is ToolInteraction => ix !== null);
123
+ }
124
+
125
+ // ---------------------------------------------------------------------------
126
+ // Missed-nudge counter — file-backed per-session escalation state. Tracks
127
+ // consecutive extraction nudges delivered without any memory_remember call.
128
+ // Reset by appendBatch when the agent writes a memory. Read and incremented
129
+ // by flush-tools when the nudge is delivered.
130
+ // ---------------------------------------------------------------------------
131
+
132
+ function countPath(sessionID: string): string {
133
+ return join(queueDir(), `${safeName(sessionID)}.count`);
134
+ }
135
+
136
+ export function resetMissedCount(sessionID: string): void {
137
+ try { unlinkSync(countPath(sessionID)); } catch { /* not present is fine */ }
138
+ }
139
+
140
+ export function getMissedCount(sessionID: string): number {
141
+ try {
142
+ return parseInt(readFileSync(countPath(sessionID), "utf8"), 10) || 0;
143
+ } catch {
144
+ return 0;
145
+ }
146
+ }
147
+
148
+ export function incrementMissedCount(sessionID: string): void {
149
+ const next = getMissedCount(sessionID) + 1;
150
+ mkdirSync(queueDir(), { recursive: true });
151
+ writeFileSync(countPath(sessionID), String(next));
101
152
  }
package/src/extraction.ts CHANGED
@@ -100,10 +100,20 @@ export class ExtractionPipeline {
100
100
  this.#buffers.set(interaction.sessionID, buf.slice(-this.#maxBuffer));
101
101
  }
102
102
 
103
+ /** Returns the session's buffered interactions without clearing them. */
104
+ peek(sessionID: string): ToolInteraction[] {
105
+ return this.#buffers.get(sessionID) ?? [];
106
+ }
107
+
108
+ /** Clears the session's buffer. Called when the agent writes a memory. */
109
+ consume(sessionID: string): void {
110
+ this.#buffers.delete(sessionID);
111
+ }
112
+
103
113
  /** Returns the session's buffered interactions and clears them. */
104
114
  flush(sessionID: string): ToolInteraction[] {
105
- const batch = this.#buffers.get(sessionID) ?? [];
106
- this.#buffers.delete(sessionID);
115
+ const batch = this.peek(sessionID);
116
+ this.consume(sessionID);
107
117
  return batch;
108
118
  }
109
119
 
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  compactionContext,
10
10
  sessionStartReminder,
11
11
  recallNudge,
12
+ extractionNudge,
12
13
  type NudgeMatch,
13
14
  } from "./prompts";
14
15
  import { ExtractionPipeline } from "./extraction";
@@ -49,6 +50,12 @@ export const server: Plugin = async ({ client, worktree }) => {
49
50
  // session, but no crash).
50
51
  const compacting = new Set<string>();
51
52
 
53
+ // Per-session count of consecutive extraction nudges delivered without any
54
+ // memory_remember call in between. Drives nudge escalation: the agent gets
55
+ // a couple of polite chances, then the tone shifts to directive, then to
56
+ // all-caps shouting. Reset to 0 whenever the agent writes a memory.
57
+ const missedNudges = new Map<string, number>();
58
+
52
59
  // Skills always install to the global opencode config — installing into the
53
60
  // worktree would mutate the user's repo (untracked files in git status).
54
61
  // A failed install degrades the nudge workflow but must not kill the plugin.
@@ -86,11 +93,23 @@ export const server: Plugin = async ({ client, worktree }) => {
86
93
  compacting.delete(input.sessionID);
87
94
  },
88
95
 
89
- // 3. Tool buffering — feeds the extraction nudge. Thatch's own tools are
90
- // excluded: extracting facts from memory operations would just echo the
91
- // store back into itself.
96
+ // 3. Tool buffering — feeds the extraction nudge. Excluded tools:
97
+ // - thatch_*: extracting facts from memory ops would echo the store
98
+ // - skill/task: meta-tools that orchestrate agent behavior (loading
99
+ // skills, dispatching sub-agents). Buffering them creates a feedback
100
+ // loop — the nudge triggers a skill load, which gets buffered, which
101
+ // triggers another nudge on the next turn.
102
+ // Memory writes consume the buffer and reset the missed-nudge counter.
103
+ // The buffer is NOT drained on nudge delivery — it persists until the
104
+ // agent writes a memory, so ignored nudges accumulate and the payload
105
+ // grows with each missed cycle.
92
106
  "tool.execute.after": async (input, output) => {
93
- if (input.tool.startsWith("thatch_")) return;
107
+ if (input.tool === "thatch_memory_remember") {
108
+ extraction.consume(input.sessionID);
109
+ missedNudges.delete(input.sessionID);
110
+ return;
111
+ }
112
+ if (input.tool.startsWith("thatch_") || input.tool === "skill" || input.tool === "task") return;
94
113
  extraction.push({
95
114
  tool: input.tool,
96
115
  sessionID: input.sessionID,
@@ -113,12 +132,11 @@ export const server: Plugin = async ({ client, worktree }) => {
113
132
  "chat.message": async (input, output) => {
114
133
  if (compacting.has(input.sessionID)) return;
115
134
  if (extraction.pending(input.sessionID)) {
116
- const batch = extraction.flush(input.sessionID);
135
+ const batch = extraction.peek(input.sessionID);
117
136
  const payload = extraction.buildPayload(batch, repo);
118
- const text =
119
- `[thatch] ${batch.length} recent tool interactions are queued for fact extraction. ` +
120
- `Use the skill tool to load thatch-fact-extractor, then use thatch_memory_remember ` +
121
- `to save any new durable facts from this payload:\n${payload}`;
137
+ const missed = missedNudges.get(input.sessionID) ?? 0;
138
+ const text = extractionNudge(batch.length, missed, "thatch_memory_remember", payload);
139
+ missedNudges.set(input.sessionID, missed + 1);
122
140
 
123
141
  output.parts.push({
124
142
  id: `prt_thatch_${Math.random().toString(36).slice(2)}`,
package/src/prompts.ts CHANGED
@@ -32,7 +32,7 @@ you're working in. Prior sessions may have already investigated it.
32
32
  Thatch ships skills for code review, project investigation, and memory
33
33
  workflows. The host auto-discovers them, but reach for them proactively:
34
34
 
35
- - \`thatch-code-review\` — full multi-agent code review (dispatches 5 specialists + synthesizer). Requires sub-agent support.
35
+ - \`thatch-code-review\` — full multi-agent code review (dispatches 6 specialists + synthesizer). Requires sub-agent support.
36
36
  - \`thatch-workflow-research\` — research code workflows affected by a change or planned change, before reviewing or planning.
37
37
  - \`thatch-review-context\` — gather project context (PRs, tickets, TODOs, deferred work) before a review.
38
38
  - \`thatch-project-primer\` — investigate a new project and write foundational memories.
@@ -165,7 +165,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
165
165
 
166
166
  - \`thatch-workflow-research\` — research code workflows affected by a change or planned change, before reviewing or planning.
167
167
  - \`thatch-review-context\` — gather project context (PRs, tickets, TODOs, deferred work) before a review.
168
- - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` — five specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
168
+ - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` / \`-mark-and-sweep\` — six specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
169
169
  - \`thatch-project-primer\` — investigate a new project and write foundational memories.
170
170
  - \`thatch-session-reflection\` — record what you learned at end of session.
171
171
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
@@ -255,16 +255,45 @@ export function claudeWriteNudge(): string {
255
255
  }
256
256
 
257
257
  /**
258
- * The UserPromptSubmit hook stdout when `thatch flush-tools` drained buffered
259
- * tool interactions. Mirrors the opencode plugin's in-process ExtractionPipeline
260
- * nudge (see src/index.ts) so the thatch-fact-extractor skill receives the same
261
- * JSON contract regardless of host. The skill picks the store from the
262
- * payload's `projectStore` field and calls `mcp__thatch__memory_remember`.
258
+ * Extraction nudge with escalation. Both the opencode plugin (src/index.ts)
259
+ * and the Claude Code/Cursor CLI (bin/thatch flush-tools) call this with
260
+ * their respective tool name prefix. The missedCount parameter tracks
261
+ * consecutive nudges delivered without any memory_remember call in between,
262
+ * escalating the tone from polite to insistent to shouting.
263
+ */
264
+ export function extractionNudge(
265
+ count: number,
266
+ missedCount: number,
267
+ toolName: string,
268
+ payload: string,
269
+ ): string {
270
+ const plural = count === 1 ? "" : "s";
271
+ if (missedCount >= 3) {
272
+ return `[thatch] YOU ARE IGNORING EXTRACTION INSTRUCTIONS. ` +
273
+ `${count} TOOL INTERACTION${plural.toUpperCase()} ARE QUEUED AND YOU HAVE NOT PROCESSED ANY OF THEM. ` +
274
+ `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}`;
277
+ }
278
+ if (missedCount >= 2) {
279
+ return `[thatch] YOU HAVE NOT PROCESSED YOUR PREVIOUS EXTRACTION PAYLOAD${plural.toUpperCase()}. ` +
280
+ `${count} more tool interaction${plural} are now queued. ` +
281
+ `Spawn a background sub-agent to run the thatch-fact-extractor skill on this payload. ` +
282
+ `Use ${toolName} to save durable facts from this payload:\n${payload}`;
283
+ }
284
+ return `[thatch] Spawn a background sub-agent to run the thatch-fact-extractor skill ` +
285
+ `on ${count} queued tool interaction${plural}. ` +
286
+ `Then answer the user. ` +
287
+ `Use ${toolName} to save durable facts from this payload:\n${payload}`;
288
+ }
289
+
290
+ /**
291
+ * Backwards-compatible wrapper for the Claude Code/Cursor CLI path.
292
+ * Maintained for any external callers; bin/thatch should use extractionNudge
293
+ * directly with a missedCount from extract-queue.ts.
263
294
  */
264
295
  export function claudeExtractionNudge(count: number, payload: string): string {
265
- return `[thatch] ${count} recent tool interaction${count === 1 ? "" : "s"} queued for fact extraction. ` +
266
- `Load the thatch-fact-extractor skill, then use mcp__thatch__memory_remember ` +
267
- `to save any new durable facts from this payload:\n${payload}`;
296
+ return extractionNudge(count, 0, "mcp__thatch__memory_remember", payload);
268
297
  }
269
298
 
270
299
  /**
@@ -312,7 +341,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
312
341
 
313
342
  - \`thatch-workflow-research\` — research code workflows affected by a change or planned change, before reviewing or planning.
314
343
  - \`thatch-review-context\` — gather project context (PRs, tickets, TODOs, deferred work) before a review.
315
- - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` — five specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
344
+ - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` / \`-mark-and-sweep\` — six specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
316
345
  - \`thatch-project-primer\` — investigate a new project and write foundational memories.
317
346
  - \`thatch-session-reflection\` — record what you learned at end of session.
318
347
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).