@jeffober/thatch 0.1.22 → 0.1.25

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/src/index.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  sessionStartReminder,
11
11
  recallNudge,
12
12
  extractionNudge,
13
+ extractionDirectPrompt,
13
14
  predictionNudge,
14
15
  type NudgeMatch,
15
16
  } from "./prompts";
@@ -81,6 +82,32 @@ export const server: Plugin = async ({ client, worktree }) => {
81
82
  // any tool calls the parent made concurrently with the sub-agent.
82
83
  const parentSnapshots = new Map<string, ToolInteraction[]>();
83
84
 
85
+ // Parent sessions with an active direct-extraction child. When the parent
86
+ // goes idle with pending tool interactions, the plugin creates a child
87
+ // session and prompts it directly (via the SDK client) instead of injecting
88
+ // a nudge into the next user message. This set suppresses the nudge path
89
+ // while the child runs, and prevents re-triggering if the parent goes idle
90
+ // again before the child finishes. Cleared when the child goes idle, errors,
91
+ // or is deleted. If direct extraction fails, the set is cleared so the nudge
92
+ // path takes over as a fallback on the next chat.message.
93
+ const extracting = new Set<string>();
94
+
95
+ // Child session IDs created by triggerExtraction (direct extraction only).
96
+ // The child idle handler uses this to distinguish plugin-created extraction
97
+ // children from task-dispatched sub-agents (code review specialists, the
98
+ // nudge-path fact-extractor, any model-dispatched task). Only extraction
99
+ // children get the full cleanup: buffer drain, session deletion, toast.
100
+ // Non-extraction children get the old behavior (completeAccepted +
101
+ // missedNudges.reset) so their sessions are not deleted out from under the
102
+ // task tool that dispatched them.
103
+ const extractionChildren = new Set<string>();
104
+
105
+ // Per-child-session extraction metrics for the toast notification. When the
106
+ // child session goes idle, these counts are formatted into a toast that
107
+ // shows the user thatch is working without polluting the conversation.
108
+ // Keyed by child session ID. Cleaned up on child idle, error, or deletion.
109
+ const childMetrics = new Map<string, { new: number; updated: number; deleted: number }>();
110
+
84
111
  // Skills always install to the global opencode config — installing into the
85
112
  // worktree would mutate the user's repo (untracked files in git status).
86
113
  // A failed install degrades the nudge workflow but must not kill the plugin.
@@ -96,6 +123,83 @@ export const server: Plugin = async ({ client, worktree }) => {
96
123
  const sys = systemPrompt(repo);
97
124
  const compact = compactionContext(repo);
98
125
 
126
+ // Direct extraction: create a child session linked to the parent and prompt
127
+ // it with the extraction payload. The child runs the fact-extractor skill,
128
+ // writes memories, and goes idle. The existing childToParent / snapshot
129
+ // drain machinery handles buffer cleanup. The nudge path is a fallback if
130
+ // this throws.
131
+ //
132
+ // Does NOT call extraction.accept — entries stay in pending so consumeSnapshot
133
+ // can drain them by reference identity when the child writes a memory. The
134
+ // extracting set (not accept) suppresses the nudge in chat.message.
135
+ //
136
+ // The child ID is added to extractionChildren so the idle handler can
137
+ // distinguish plugin-created extraction children from task-dispatched
138
+ // sub-agents. Only extraction children get the full cleanup (buffer drain,
139
+ // session deletion, toast).
140
+ async function triggerExtraction(parentID: string): Promise<void> {
141
+ extracting.add(parentID);
142
+
143
+ const batch = extraction.peek(parentID);
144
+ const payload = extraction.buildPayload(batch, repo);
145
+ const promptText = extractionDirectPrompt(batch.length, payload);
146
+
147
+ const result = await client.session.create({
148
+ body: { parentID, title: "thatch-extraction" },
149
+ });
150
+ // session.created event fires here, setting childToParent and
151
+ // parentSnapshots (snapshot of the full pending buffer, since we
152
+ // have not called accept).
153
+ const childId = result.data!.id;
154
+ extractionChildren.add(childId);
155
+
156
+ // Clean up the child session and all map entries if prompting fails.
157
+ // Without this, the child exists on the server but was never prompted,
158
+ // so it never goes idle and the maps leak.
159
+ const cleanupChild = () => {
160
+ extracting.delete(parentID);
161
+ extractionChildren.delete(childId);
162
+ childToParent.delete(childId);
163
+ parentSnapshots.delete(childId);
164
+ childMetrics.delete(childId);
165
+ try { client.session.delete({ path: { id: childId } }); } catch {}
166
+ };
167
+
168
+ const bgEnabled =
169
+ process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS === "true" ||
170
+ process.env.OPENCODE_EXPERIMENTAL === "true";
171
+
172
+ if (bgEnabled) {
173
+ try {
174
+ await client.session.promptAsync({
175
+ path: { id: childId },
176
+ body: {
177
+ parts: [{ type: "text", text: promptText }],
178
+ },
179
+ });
180
+ } catch (err) {
181
+ console.error(`[thatch] extraction promptAsync failed: ${err}`);
182
+ cleanupChild();
183
+ throw err;
184
+ }
185
+ } else {
186
+ // Fire and forget — the parent is already idle, so blocking the event
187
+ // handler would only delay other event processing. The child runs to
188
+ // completion and its idle event triggers cleanup.
189
+ client.session
190
+ .prompt({
191
+ path: { id: childId },
192
+ body: {
193
+ parts: [{ type: "text", text: promptText }],
194
+ },
195
+ })
196
+ .catch((err: unknown) => {
197
+ console.error(`[thatch] extraction child failed: ${err}`);
198
+ cleanupChild();
199
+ });
200
+ }
201
+ }
202
+
99
203
  return {
100
204
  tool: createTools(db, model, repo),
101
205
 
@@ -118,12 +222,13 @@ export const server: Plugin = async ({ client, worktree }) => {
118
222
  compacting.delete(input.sessionID);
119
223
  },
120
224
 
121
- // 3. Tool buffering — feeds the extraction nudge. Excluded tools:
225
+ // 3. Tool buffering — feeds the extraction pipeline (direct extraction
226
+ // via SDK or nudge fallback). Excluded tools:
122
227
  // - thatch_*: extracting facts from memory ops would echo the store
123
228
  // - skill/task: meta-tools that orchestrate agent behavior (loading
124
229
  // skills, dispatching sub-agents). Buffering them creates a feedback
125
- // loop — the nudge triggers a skill load, which gets buffered, which
126
- // triggers another nudge on the next turn.
230
+ // loop — extraction triggers a skill load, which gets buffered, which
231
+ // triggers another extraction on the next turn.
127
232
  // Memory writes consume the buffer and reset the missed-nudge counter.
128
233
  // The buffer is NOT drained on nudge delivery — it persists until the
129
234
  // agent writes a memory, so ignored nudges accumulate and the payload
@@ -149,20 +254,28 @@ export const server: Plugin = async ({ client, worktree }) => {
149
254
  if (input.tool === "thatch_memory_remember") {
150
255
  extraction.consume(input.sessionID);
151
256
  missedNudges.delete(input.sessionID);
152
- // If this is a child session, complete the parent's accepted entries
153
- // (the extractor confirmed it is alive and saving) and drain the
154
- // parent's snapshot entries from the pending buffer. If no snapshot
155
- // was recorded (e.g. session.created event never arrived), fall back
156
- // to draining the entire parent buffer.
257
+ // Track extraction metrics for toast display. Counted for any
258
+ // child session (both direct-extraction and nudge-path sub-agents)
259
+ // since childToParent covers both. The toast only fires for
260
+ // extraction children (extractionChildren set) on idle.
157
261
  const parentID = childToParent.get(input.sessionID);
158
262
  if (parentID) {
263
+ const metrics = childMetrics.get(input.sessionID) ?? { new: 0, updated: 0, deleted: 0 };
264
+ if (input.args?.overwrite) metrics.updated++;
265
+ else metrics.new++;
266
+ childMetrics.set(input.sessionID, metrics);
267
+ // Complete the parent's accepted entries (the extractor confirmed
268
+ // it is alive and saving) and drain the parent's snapshot entries
269
+ // from the pending buffer. If no snapshot was recorded (unreachable
270
+ // when childToParent has the entry, since both are set together in
271
+ // session.created), skip the drain rather than dropping the entire
272
+ // buffer — interleaved-turn entries that arrived while the child
273
+ // was running must survive for the next extraction cycle.
159
274
  extraction.completeAccepted(parentID);
160
275
  const snapshot = parentSnapshots.get(input.sessionID);
161
276
  if (snapshot) {
162
277
  extraction.consumeSnapshot(parentID, snapshot);
163
278
  parentSnapshots.delete(input.sessionID);
164
- } else {
165
- extraction.consume(parentID);
166
279
  }
167
280
  missedNudges.delete(parentID);
168
281
  }
@@ -186,6 +299,15 @@ export const server: Plugin = async ({ client, worktree }) => {
186
299
  missedNudges.delete(input.sessionID);
187
300
  return;
188
301
  }
302
+ // Track memory deletions in child sessions for the toast metrics.
303
+ // Same scoping as the remember handler above — only child sessions,
304
+ // not the parent or manual memory writes from the user's session.
305
+ if (input.tool === "thatch_memory_forget" && childToParent.has(input.sessionID)) {
306
+ const metrics = childMetrics.get(input.sessionID) ?? { new: 0, updated: 0, deleted: 0 };
307
+ metrics.deleted++;
308
+ childMetrics.set(input.sessionID, metrics);
309
+ return;
310
+ }
189
311
  if (input.tool.startsWith("thatch_") || input.tool === "skill" || input.tool === "task") return;
190
312
  extraction.push({
191
313
  tool: input.tool,
@@ -224,7 +346,11 @@ export const server: Plugin = async ({ client, worktree }) => {
224
346
  if (isCompactionMsg) return;
225
347
  compacting.delete(input.sessionID);
226
348
  }
227
- if (extraction.pending(input.sessionID)) {
349
+ // Extraction nudge (fallback path). Skipped when the extracting set
350
+ // is active — that means a direct-extraction child session is running
351
+ // and the plugin is handling extraction via the SDK. The nudge fires
352
+ // here only when direct extraction was never triggered or threw.
353
+ if (!extracting.has(input.sessionID) && extraction.pending(input.sessionID)) {
228
354
  const batch = extraction.peek(input.sessionID);
229
355
  const payload = extraction.buildPayload(batch, repo);
230
356
  const missed = missedNudges.get(input.sessionID) ?? 0;
@@ -272,6 +398,17 @@ export const server: Plugin = async ({ client, worktree }) => {
272
398
  text: recallNudge(matches),
273
399
  synthetic: true,
274
400
  });
401
+ try {
402
+ await client.tui.showToast({
403
+ body: {
404
+ message: `\u{1F4AD} recalled ${matches.length} memor${matches.length === 1 ? "y" : "ies"}`,
405
+ variant: "info",
406
+ duration: 3000,
407
+ },
408
+ });
409
+ } catch {
410
+ // TUI may not be connected. Best-effort.
411
+ }
275
412
  }
276
413
  } catch (err) {
277
414
  console.error(`[thatch] recall nudge failed: ${err}`);
@@ -290,6 +427,17 @@ export const server: Plugin = async ({ client, worktree }) => {
290
427
  text: predictionNudge(predItems),
291
428
  synthetic: true,
292
429
  });
430
+ try {
431
+ await client.tui.showToast({
432
+ body: {
433
+ message: `\u{1F4AD} ${predItems.length} prediction${predItems.length === 1 ? "" : "s"} surfaced`,
434
+ variant: "info",
435
+ duration: 3000,
436
+ },
437
+ });
438
+ } catch {
439
+ // TUI may not be connected. Best-effort.
440
+ }
293
441
  }
294
442
  } catch (err) {
295
443
  console.error(`[thatch] prediction nudge failed: ${err}`);
@@ -300,40 +448,130 @@ export const server: Plugin = async ({ client, worktree }) => {
300
448
  },
301
449
 
302
450
  // 5. Session-start reminder, carrying the hygiene heartbeat. Hygiene is
303
- // best-effort: a failure there must not cost the reminder itself.
451
+ // best-effort: a failure there must not cost the reminder itself.
304
452
  //
305
453
  // Also tracks parent-child session relationships for cross-session buffer
306
454
  // drain. session.created with a parentID records the mapping AND
307
- // snapshots the parent's current buffer — so the child's later
455
+ // snapshots the parent's current buffer so the child's later
308
456
  // memory_remember drains only those snapshot entries, not the parent's
309
457
  // entire buffer (which may have grown from interleaved turns).
310
458
  //
311
- // The accepted-buffer lifecycle completes or requeues on child lifecycle
312
- // events: idle means the extractor finished (a no-save run writes no
313
- // memory but still processed the payload); error or deletion before
314
- // completion means the facts were never extracted, so the entries go
315
- // back to pending. session.deleted also cleans both maps to avoid
316
- // unbounded growth.
459
+ // Child lifecycle events: idle means the extractor finished; error or
460
+ // deletion before completion means the facts were never extracted, so
461
+ // entries go back to pending. Only extraction children (created by
462
+ // triggerExtraction, tracked in extractionChildren) get the full cleanup
463
+ // (buffer drain, session deletion, toast). Task-dispatched sub-agents
464
+ // get the old behavior (completeAccepted + missedNudges.reset) so their
465
+ // sessions are not deleted out from under the task tool.
317
466
  event: async ({ event }) => {
318
467
  if (event.type === "session.created") {
319
468
  const info = event.properties.info;
320
469
  if (info.parentID) {
321
470
  childToParent.set(info.id, info.parentID);
322
471
  parentSnapshots.set(info.id, [...extraction.peek(info.parentID)]);
472
+ // Child sessions don't need the session-start reminder — only
473
+ // top-level sessions do. Returning here prevents extraction
474
+ // children from receiving the hygiene report and tool overview.
475
+ return;
323
476
  }
324
477
  }
325
478
  if (event.type === "session.error") {
326
479
  const childID = event.properties.sessionID;
327
480
  const parentID = childID ? childToParent.get(childID) : undefined;
328
- if (parentID) extraction.requeueAccepted(parentID);
481
+ if (parentID && childID) {
482
+ extraction.requeueAccepted(parentID);
483
+ extracting.delete(parentID);
484
+ childToParent.delete(childID);
485
+ parentSnapshots.delete(childID);
486
+ childMetrics.delete(childID);
487
+ extractionChildren.delete(childID);
488
+ }
329
489
  return;
330
490
  }
331
491
  if (event.type === "session.status" && event.properties.status?.type === "idle") {
332
- const childID = event.properties.sessionID;
333
- const parentID = childID ? childToParent.get(childID) : undefined;
334
- if (parentID) {
335
- extraction.completeAccepted(parentID);
336
- missedNudges.delete(parentID);
492
+ const sessionID = event.properties.sessionID;
493
+ const parentID = sessionID ? childToParent.get(sessionID) : undefined;
494
+ if (parentID && sessionID) {
495
+ // A child session went idle. Two cases:
496
+ // - Extraction child (created by triggerExtraction, tracked in
497
+ // extractionChildren): drain the parent's snapshot, fire a toast
498
+ // with metrics, delete the child session, clean up all maps.
499
+ // - Task-dispatched sub-agent (code review specialist, nudge-path
500
+ // fact-extractor, any model-dispatched task): complete the
501
+ // parent's accepted entries and reset missedNudges. Do NOT drain
502
+ // the buffer or delete the session — the task tool that
503
+ // dispatched the sub-agent needs to read its output.
504
+ if (extractionChildren.has(sessionID)) {
505
+ // Drain the parent's snapshot entries from the pending buffer.
506
+ // If the child wrote memories, consumeSnapshot already ran in
507
+ // tool.execute.after and the snapshot is gone — nothing to
508
+ // drain. If the child did a no-save run, the snapshot entries
509
+ // are still in the buffer and need to be drained here so they
510
+ // don't replay as a nudge on the next chat.message. Never drain
511
+ // the entire buffer — interleaved-turn entries must survive.
512
+ const snapshot = parentSnapshots.get(sessionID);
513
+ if (snapshot) {
514
+ extraction.consumeSnapshot(parentID, snapshot);
515
+ }
516
+
517
+ // Fire a toast with the extraction metrics. Only show a toast
518
+ // when memories were actually written — no toast for no-save
519
+ // runs to avoid notification fatigue.
520
+ const metrics = childMetrics.get(sessionID);
521
+ const parts: string[] = [];
522
+ if (metrics) {
523
+ if (metrics.new > 0) parts.push(`new: ${metrics.new}`);
524
+ if (metrics.updated > 0) parts.push(`updated: ${metrics.updated}`);
525
+ if (metrics.deleted > 0) parts.push(`deleted: ${metrics.deleted}`);
526
+ }
527
+ if (parts.length > 0) {
528
+ try {
529
+ await client.tui.showToast({
530
+ body: { message: `\u{1F4AD} ${parts.join(", ")}`, variant: "success", duration: 4000 },
531
+ });
532
+ } catch {
533
+ // TUI may not be connected (e.g. headless mode).
534
+ }
535
+ }
536
+
537
+ extraction.completeAccepted(parentID);
538
+ missedNudges.delete(parentID);
539
+ extracting.delete(parentID);
540
+ childToParent.delete(sessionID);
541
+ parentSnapshots.delete(sessionID);
542
+ childMetrics.delete(sessionID);
543
+ extractionChildren.delete(sessionID);
544
+ extraction.consume(sessionID);
545
+ // Delete the child session to avoid clutter.
546
+ try {
547
+ await client.session.delete({ path: { id: sessionID } });
548
+ } catch {
549
+ // Best-effort — the child is idle and harmless if not deleted.
550
+ }
551
+ } else {
552
+ // Task-dispatched sub-agent went idle. Complete the parent's
553
+ // accepted entries (from the nudge-path extraction_done accept)
554
+ // and reset missedNudges. Retained for the nudge fallback path.
555
+ extraction.completeAccepted(parentID);
556
+ missedNudges.delete(parentID);
557
+ }
558
+ return;
559
+ }
560
+ // Parent went idle — trigger direct extraction if there are pending
561
+ // tool interactions and no extraction is already running. Falls
562
+ // back to the nudge path on the next chat.message if this throws.
563
+ if (
564
+ sessionID &&
565
+ !compacting.has(sessionID) &&
566
+ !extracting.has(sessionID) &&
567
+ extraction.pending(sessionID)
568
+ ) {
569
+ try {
570
+ await triggerExtraction(sessionID);
571
+ } catch (err) {
572
+ console.error(`[thatch] direct extraction trigger failed: ${err}`);
573
+ extracting.delete(sessionID);
574
+ }
337
575
  }
338
576
  return;
339
577
  }
@@ -341,11 +579,17 @@ export const server: Plugin = async ({ client, worktree }) => {
341
579
  const id = event.properties.info.id;
342
580
  // A child deleted before completing never processed its payload.
343
581
  const parentID = childToParent.get(id);
344
- if (parentID) extraction.requeueAccepted(parentID);
582
+ if (parentID) {
583
+ extraction.requeueAccepted(parentID);
584
+ extracting.delete(parentID);
585
+ }
345
586
  childToParent.delete(id);
346
587
  parentSnapshots.delete(id);
588
+ childMetrics.delete(id);
589
+ extractionChildren.delete(id);
347
590
  // A deleted parent takes its accepted entries with it.
348
591
  extraction.completeAccepted(id);
592
+ extracting.delete(id);
349
593
  return;
350
594
  }
351
595
 
package/src/prompts.ts CHANGED
@@ -37,7 +37,7 @@ you're working in. Prior sessions may have already investigated it.
37
37
  Thatch ships skills for code review, project investigation, and memory
38
38
  workflows. The host auto-discovers them, but reach for them proactively:
39
39
 
40
- - \`thatch-code-review\` — full multi-agent code review (dispatches 6 specialists + synthesizer). Requires sub-agent support.
40
+ - \`thatch-code-review\` — full multi-agent code review (dispatches 7 specialists + synthesizer). Requires sub-agent support.
41
41
  - \`thatch-workflow-research\` — research code workflows affected by a change or planned change, before reviewing or planning.
42
42
  - \`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.
43
43
  - \`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.
@@ -76,6 +76,11 @@ that thread. If the user's last message has been fully addressed, stop and
76
76
  wait. Do not treat the completion as a trigger to advance work the user has
77
77
  not greenlit.
78
78
 
79
+ If a background task was cancelled after timing out, re-dispatch it once with
80
+ the same prompt. If the re-dispatch also fails or times out, note the gap and
81
+ move on. Do not retry more than once — repeated failures indicate an
82
+ intractable prompt, not a transient hang.
83
+
79
84
  ## User Decision Model
80
85
 
81
86
  A statistical model of the user's decision-making preferences is maintained by
@@ -238,7 +243,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
238
243
  - \`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.
239
244
  - \`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.
240
245
  - \`thatch-review-context\` — gather project context (PRs, tickets, TODOs, deferred work) before a review.
241
- - \`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.
246
+ - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` / \`-mark-and-sweep\` / \`-highlights\` — seven specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
242
247
  - \`thatch-project-primer\` — investigate a new project and write foundational memories.
243
248
  - \`thatch-session-reflection\` — record what you learned at end of session.
244
249
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
@@ -432,6 +437,23 @@ export function extractionNudge(
432
437
  `Use ${toolName} to save durable facts from this payload:\n${payload}`;
433
438
  }
434
439
 
440
+ /**
441
+ * Prompt text sent directly to a child session the plugin created for
442
+ * extraction. Unlike extractionNudge (which tells the model to dispatch a
443
+ * sub-agent and call extraction_done), this prompt is already inside the
444
+ * child — the model just runs the fact-extractor skill and saves facts.
445
+ * The payload format is identical to the nudge path so the skill receives
446
+ * the same contract either way.
447
+ */
448
+ export function extractionDirectPrompt(count: number, payload: string): string {
449
+ const plural = count === 1 ? "" : "s";
450
+ return `Run the thatch-fact-extractor skill to extract durable facts from ` +
451
+ `${count} queued tool interaction${plural}. ` +
452
+ `Use thatch_memory_remember to save any durable facts you find. ` +
453
+ `Call thatch_extraction_done when finished, even if nothing was worth saving.\n` +
454
+ `Payload:\n${payload}`;
455
+ }
456
+
435
457
  /**
436
458
  * Backwards-compatible wrapper for the Claude Code/Cursor CLI path.
437
459
  * Maintained for any external callers; bin/thatch should use extractionNudge
@@ -489,7 +511,7 @@ workflows. The host auto-discovers them, but reach for them proactively:
489
511
  - \`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.
490
512
  - \`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.
491
513
  - \`thatch-review-context\` — gather project context (PRs, tickets, TODOs, deferred work) before a review.
492
- - \`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.
514
+ - \`thatch-review-pedantic\` / \`-acceptance\` / \`-state-flow\` / \`-no-slop\` / \`-breadcrumbs\` / \`-mark-and-sweep\` / \`-highlights\` — seven specialist review lenses. Run individually, then \`thatch-review-synthesizer\` to verify and aggregate.
493
515
  - \`thatch-project-primer\` — investigate a new project and write foundational memories.
494
516
  - \`thatch-session-reflection\` — record what you learned at end of session.
495
517
  - \`thatch-fact-extractor\` — extract durable facts from recent tool interactions (auto-triggered by the extraction pipeline).
package/src/skills.ts CHANGED
@@ -39,6 +39,7 @@ function loadSharedSkills(): SkillDef[] {
39
39
  "thatch-review-no-slop",
40
40
  "thatch-review-breadcrumbs",
41
41
  "thatch-review-mark-and-sweep",
42
+ "thatch-review-highlights",
42
43
  "thatch-review-synthesizer",
43
44
  "thatch-review-context",
44
45
  "thatch-workflow-research",