@jeffober/thatch 0.1.22 → 0.1.24
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 +5 -2
- package/artifacts/skills/common.md +6 -0
- package/artifacts/skills/thatch-code-review.md +20 -7
- package/artifacts/skills/thatch-review-acceptance.md +17 -1
- package/artifacts/skills/thatch-review-context.md +2 -0
- package/artifacts/skills/thatch-review-highlights.md +111 -0
- package/artifacts/skills/thatch-review-synthesizer.md +29 -4
- package/docs/dev/README.md +43 -14
- package/docs/dev/gotchas.md +29 -7
- package/docs/dev/mcp-parity.md +29 -9
- package/docs/dev/setup-and-hooks.md +14 -3
- package/docs/dev/skills.md +8 -7
- package/docs/qa/use-cases/README.md +1 -1
- package/docs/qa/use-cases/UC-003-extraction-nudge.md +33 -13
- package/docs/qa/use-cases/UC-005-setup-install.md +2 -2
- package/docs/qa/use-cases/UC-014-skill-install-drift.md +3 -3
- package/docs/qa/use-cases/UC-020-extraction-escalation.md +16 -4
- package/docs/user/README.md +24 -16
- package/package.json +1 -1
- package/src/index.ts +270 -26
- package/src/prompts.ts +25 -3
- package/src/skills.ts +1 -0
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
|
|
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 —
|
|
126
|
-
// triggers another
|
|
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
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
//
|
|
316
|
-
//
|
|
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)
|
|
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
|
|
333
|
-
const parentID =
|
|
334
|
-
if (parentID) {
|
|
335
|
-
|
|
336
|
-
|
|
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)
|
|
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
|
|
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\` —
|
|
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\` —
|
|
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