@henryqw/pi-session-recall 2.0.0 → 2.1.1
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 +70 -6
- package/docs/session-search-routing.html +138 -0
- package/docs/session-search-routing.svg +121 -0
- package/extensions/hydrate.ts +16 -3
- package/extensions/repository-inventory.ts +515 -0
- package/extensions/search-core.ts +71 -2
- package/extensions/session-recall.ts +220 -4
- package/extensions/types.ts +6 -0
- package/package.json +2 -1
- package/skills/pi-session-pattern-miner/SKILL.md +58 -16
|
@@ -9,15 +9,18 @@ import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
9
9
|
import { Type } from "typebox";
|
|
10
10
|
import { realpathSync } from "node:fs";
|
|
11
11
|
import { join, sep } from "node:path";
|
|
12
|
-
import { getSessionRows, searchIndex, syncSessions } from "./search-core.ts";
|
|
12
|
+
import { getPreparationRows, getSessionRows, searchIndex, syncSessions } from "./search-core.ts";
|
|
13
13
|
import { getWindow, readSession } from "./hydrate.ts";
|
|
14
14
|
import { MAX_QUERY_CHARS } from "./query.ts";
|
|
15
|
-
import type
|
|
15
|
+
import { inventoryRepository, type RepositoryInventory } from "./repository-inventory.ts";
|
|
16
|
+
import type { PreparationSessionRow, WindowMessage } from "./types.ts";
|
|
16
17
|
|
|
17
18
|
const dbPath = () => join(extensionConfigDir("pi-session-recall"), "index.db");
|
|
18
19
|
const sessionsDir = () => join(getAgentDir(), "sessions");
|
|
19
20
|
|
|
20
21
|
const OUTPUT_CHAR_BUDGET = 50_000;
|
|
22
|
+
const INVENTORY_CHAR_BUDGET = 10_000;
|
|
23
|
+
const ERROR_MESSAGE_CHARS = 512;
|
|
21
24
|
|
|
22
25
|
function clamp(n: number | undefined, min: number, max: number, dflt: number): number {
|
|
23
26
|
if (typeof n !== "number" || !Number.isFinite(n)) return dflt;
|
|
@@ -78,7 +81,185 @@ function boundContent(
|
|
|
78
81
|
return cap === null ? build(null) : { ...build(cap), contentTruncated: true };
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
type InventoryCollection = "packageScripts" | "executableScripts" | "skills" | "agentInstructions";
|
|
85
|
+
const INVENTORY_COLLECTIONS: InventoryCollection[] = ["packageScripts", "executableScripts", "skills", "agentInstructions"];
|
|
86
|
+
|
|
87
|
+
function inventoryShape(
|
|
88
|
+
source: RepositoryInventory,
|
|
89
|
+
kept: Record<InventoryCollection, unknown[]>,
|
|
90
|
+
): Record<string, unknown> {
|
|
91
|
+
const omittedCounts = {
|
|
92
|
+
packageScripts: source.packageScripts.length - kept.packageScripts.length,
|
|
93
|
+
executableScripts: source.executableScripts.length - kept.executableScripts.length,
|
|
94
|
+
skills: source.skills.length - kept.skills.length,
|
|
95
|
+
agentInstructions: source.agentInstructions.length - kept.agentInstructions.length,
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
available: source.available,
|
|
99
|
+
...(source.reason ? { reason: source.reason } : {}),
|
|
100
|
+
...(source.provenance ? { provenance: source.provenance } : {}),
|
|
101
|
+
worktreeVerified: source.worktreeVerified,
|
|
102
|
+
packageScripts: kept.packageScripts,
|
|
103
|
+
executableScripts: kept.executableScripts,
|
|
104
|
+
skills: kept.skills,
|
|
105
|
+
agentInstructions: kept.agentInstructions,
|
|
106
|
+
truncated: Object.values(omittedCounts).some((count) => count > 0),
|
|
107
|
+
omittedCounts,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Keep stable prefixes from every inventory collection within one bounded,
|
|
112
|
+
* round-robin allocation so a large first collection cannot starve the rest. */
|
|
113
|
+
function boundInventory(source: RepositoryInventory, maxChars: number): Record<string, unknown> {
|
|
114
|
+
const all: Record<InventoryCollection, unknown[]> = {
|
|
115
|
+
packageScripts: source.packageScripts,
|
|
116
|
+
executableScripts: source.executableScripts,
|
|
117
|
+
skills: source.skills,
|
|
118
|
+
agentInstructions: source.agentInstructions,
|
|
119
|
+
};
|
|
120
|
+
const full = inventoryShape(source, all);
|
|
121
|
+
if (JSON.stringify(full).length <= maxChars) return full;
|
|
122
|
+
|
|
123
|
+
const kept: Record<InventoryCollection, unknown[]> = {
|
|
124
|
+
packageScripts: [],
|
|
125
|
+
executableScripts: [],
|
|
126
|
+
skills: [],
|
|
127
|
+
agentInstructions: [],
|
|
128
|
+
};
|
|
129
|
+
const blocked = new Set<InventoryCollection>();
|
|
130
|
+
for (;;) {
|
|
131
|
+
let advanced = false;
|
|
132
|
+
for (const key of INVENTORY_COLLECTIONS) {
|
|
133
|
+
if (blocked.has(key) || kept[key].length >= all[key].length) continue;
|
|
134
|
+
const candidate = { ...kept, [key]: [...kept[key], all[key][kept[key].length]] };
|
|
135
|
+
if (JSON.stringify(inventoryShape(source, candidate)).length <= maxChars) {
|
|
136
|
+
kept[key] = candidate[key];
|
|
137
|
+
advanced = true;
|
|
138
|
+
} else {
|
|
139
|
+
blocked.add(key);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!advanced) break;
|
|
143
|
+
}
|
|
144
|
+
return inventoryShape(source, kept);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface PreparedSession {
|
|
148
|
+
metadata: Record<string, unknown>;
|
|
149
|
+
messages: WindowMessage[];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hydrationError(error: unknown): { kind: "missing" | "oversized" | "unreadable"; message: string } {
|
|
153
|
+
const message = (error instanceof Error ? error.message : String(error)).slice(0, ERROR_MESSAGE_CHARS);
|
|
154
|
+
const code = (error as NodeJS.ErrnoException)?.code;
|
|
155
|
+
return {
|
|
156
|
+
kind: code === "ENOENT" ? "missing" : message.includes("exceeds 32 MiB snapshot limit") ? "oversized" : "unreadable",
|
|
157
|
+
message,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function hydratePreparationSession(row: PreparationSessionRow): PreparedSession {
|
|
162
|
+
const indexed = {
|
|
163
|
+
path: row.path,
|
|
164
|
+
cwd: row.cwd,
|
|
165
|
+
name: row.name ?? null,
|
|
166
|
+
startedAt: row.startedAt ?? null,
|
|
167
|
+
lineageId: row.lineageId,
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const hydrated = readSession(row.path, 20, 10, { userAssistantTextOnly: true });
|
|
171
|
+
return {
|
|
172
|
+
metadata: {
|
|
173
|
+
...indexed,
|
|
174
|
+
branchTip: hydrated.branchTip,
|
|
175
|
+
totalMessages: hydrated.totalMessages,
|
|
176
|
+
truncated: hydrated.truncated,
|
|
177
|
+
contentTruncated: false,
|
|
178
|
+
messages: [],
|
|
179
|
+
},
|
|
180
|
+
messages: hydrated.messages,
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return {
|
|
184
|
+
metadata: {
|
|
185
|
+
...indexed,
|
|
186
|
+
branchTip: null,
|
|
187
|
+
totalMessages: null,
|
|
188
|
+
truncated: false,
|
|
189
|
+
contentTruncated: false,
|
|
190
|
+
messages: [],
|
|
191
|
+
error: hydrationError(error),
|
|
192
|
+
},
|
|
193
|
+
messages: [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function allocatePreparationMessages(session: PreparedSession, budget: number): Record<string, unknown> {
|
|
199
|
+
if (session.messages.length === 0) return session.metadata;
|
|
200
|
+
if (JSON.stringify(session.messages).length - 2 <= budget) {
|
|
201
|
+
return { ...session.metadata, messages: session.messages };
|
|
202
|
+
}
|
|
203
|
+
const maxLen = Math.max(...session.messages.map((message) => message.content.length), 0);
|
|
204
|
+
const cap = maxFittingCap(maxLen, budget + 2, (value) => truncateContent(session.messages, value));
|
|
205
|
+
return {
|
|
206
|
+
...session.metadata,
|
|
207
|
+
contentTruncated: true,
|
|
208
|
+
messages: cap === null ? [] : truncateContent(session.messages, cap),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildPreparationResult(
|
|
213
|
+
kind: "repository" | "all",
|
|
214
|
+
requestedLimit: number,
|
|
215
|
+
gitRoot: string | undefined,
|
|
216
|
+
syncResult: ReturnType<typeof syncSessions>,
|
|
217
|
+
rows: PreparationSessionRow[],
|
|
218
|
+
repositoryInventory: RepositoryInventory,
|
|
219
|
+
): Record<string, unknown> {
|
|
220
|
+
const sync = {
|
|
221
|
+
walkComplete: syncResult.walkComplete,
|
|
222
|
+
backlogRemaining: syncResult.backlogRemaining,
|
|
223
|
+
complete: syncResult.walkComplete && syncResult.backlogRemaining === 0,
|
|
224
|
+
};
|
|
225
|
+
const sessions = rows.map(hydratePreparationSession);
|
|
226
|
+
const emptyCollections: Record<InventoryCollection, unknown[]> = {
|
|
227
|
+
packageScripts: [],
|
|
228
|
+
executableScripts: [],
|
|
229
|
+
skills: [],
|
|
230
|
+
agentInstructions: [],
|
|
231
|
+
};
|
|
232
|
+
const minimumInventory = inventoryShape(repositoryInventory, emptyCollections);
|
|
233
|
+
const build = (
|
|
234
|
+
preparedSessions: Record<string, unknown>[],
|
|
235
|
+
inventory: Record<string, unknown>,
|
|
236
|
+
contentTruncated: boolean,
|
|
237
|
+
) => ({
|
|
238
|
+
mode: "prepare-pattern-miner",
|
|
239
|
+
scope: { kind, gitRoot: gitRoot ?? null, requestedLimit, sampledCount: preparedSessions.length },
|
|
240
|
+
sync,
|
|
241
|
+
sessions: preparedSessions,
|
|
242
|
+
inventory,
|
|
243
|
+
contentTruncated,
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const metadataOnlyLength = JSON.stringify(build(sessions.map((session) => session.metadata), minimumInventory, false)).length;
|
|
247
|
+
if (metadataOnlyLength > OUTPUT_CHAR_BUDGET) throw new Error("Pattern-miner preparation metadata exceeds output budget.");
|
|
248
|
+
const inventoryBudget = Math.min(
|
|
249
|
+
INVENTORY_CHAR_BUDGET,
|
|
250
|
+
JSON.stringify(minimumInventory).length + OUTPUT_CHAR_BUDGET - metadataOnlyLength,
|
|
251
|
+
);
|
|
252
|
+
const inventory = boundInventory(repositoryInventory, inventoryBudget);
|
|
253
|
+
const baseLength = JSON.stringify(build(sessions.map((session) => session.metadata), inventory, false)).length;
|
|
254
|
+
const perSessionBudget = sessions.length === 0 ? 0 : Math.floor((OUTPUT_CHAR_BUDGET - baseLength) / sessions.length);
|
|
255
|
+
const allocated = sessions.map((session) => allocatePreparationMessages(session, perSessionBudget));
|
|
256
|
+
const contentTruncated = allocated.some((session) => session.contentTruncated === true);
|
|
257
|
+
return build(allocated, inventory, contentTruncated);
|
|
258
|
+
}
|
|
259
|
+
|
|
81
260
|
interface ToolParams {
|
|
261
|
+
operation?: "prepare-pattern-miner";
|
|
262
|
+
scope?: "repository" | "all";
|
|
82
263
|
query?: string;
|
|
83
264
|
sessionId?: string;
|
|
84
265
|
aroundMessageId?: string;
|
|
@@ -90,6 +271,7 @@ interface ToolParams {
|
|
|
90
271
|
|
|
91
272
|
const DESCRIPTION = `Search past Pi sessions locally with FTS5; returns stored messages.
|
|
92
273
|
|
|
274
|
+
- \`operation: "prepare-pattern-miner"\` + \`scope\`: prepare one bounded corpus and repository inventory.
|
|
93
275
|
- \`query\`: discover matches. Prefer distinctive identifiers or uncommon terms; multi-word queries are AND. Use \`OR\`/\`NOT\` for Boolean queries and quotes only when exact wording is known.
|
|
94
276
|
- \`sessionId\` + \`aroundMessageId\`: scroll ±\`window\`; retain \`branchTip\` across forks.
|
|
95
277
|
- \`sessionId\` alone: read; no args: browse recent sessions.
|
|
@@ -117,12 +299,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
117
299
|
"Use session_search only when the user explicitly asks about past Pi sessions, historical decisions, or repeated work not available in the current conversation. Do not use it for current-session continuation or ordinary repository inspection.",
|
|
118
300
|
],
|
|
119
301
|
parameters: Type.Object({
|
|
302
|
+
operation: Type.Optional(StringEnum(["prepare-pattern-miner"] as const)),
|
|
303
|
+
scope: Type.Optional(StringEnum(["repository", "all"] as const)),
|
|
120
304
|
query: Type.Optional(Type.String({ description: "Search query (discovery). FTS5 syntax supported." })),
|
|
121
305
|
sessionId: Type.Optional(Type.String({ description: "Absolute path of the session file." })),
|
|
122
306
|
aroundMessageId: Type.Optional(Type.String({ description: "Anchor entry id for scroll mode — centers the window (with sessionId)." })),
|
|
123
307
|
branchTip: Type.Optional(Type.String({ description: "Branch tip entry id from a previous response — selects which branch of a forked session to scroll; aroundMessageId must lie on it." })),
|
|
124
308
|
window: Type.Optional(Type.Number({ description: "Scroll window radius, [1,20], default 5." })),
|
|
125
|
-
limit: Type.Optional(Type.Number({ description: "Max results, [1,10]
|
|
309
|
+
limit: Type.Optional(Type.Number({ description: "Max results, [1,10]. Defaults to 10 for preparation and 3 otherwise." })),
|
|
126
310
|
detail: Type.Optional(StringEnum(["adaptive", "full"] as const)),
|
|
127
311
|
}),
|
|
128
312
|
renderResult(result, { expanded }, theme) {
|
|
@@ -140,8 +324,39 @@ export default function (pi: ExtensionAPI): void {
|
|
|
140
324
|
invalidate() {},
|
|
141
325
|
};
|
|
142
326
|
},
|
|
143
|
-
async execute(_toolCallId, rawParams: ToolParams,
|
|
327
|
+
async execute(_toolCallId, rawParams: ToolParams, signal, _onUpdate, ctx) {
|
|
144
328
|
try {
|
|
329
|
+
if (rawParams.operation !== undefined && rawParams.operation !== "prepare-pattern-miner") {
|
|
330
|
+
throw new Error("Unsupported session_search operation.");
|
|
331
|
+
}
|
|
332
|
+
if (rawParams.scope !== undefined && rawParams.operation === undefined) {
|
|
333
|
+
throw new Error("scope requires operation: prepare-pattern-miner.");
|
|
334
|
+
}
|
|
335
|
+
if (rawParams.operation === "prepare-pattern-miner") {
|
|
336
|
+
const incompatible = (["query", "sessionId", "aroundMessageId", "branchTip", "window", "detail"] as const)
|
|
337
|
+
.filter((key) => rawParams[key] !== undefined);
|
|
338
|
+
if (incompatible.length > 0) {
|
|
339
|
+
throw new Error(`prepare-pattern-miner does not accept: ${incompatible.join(", ")}.`);
|
|
340
|
+
}
|
|
341
|
+
if (rawParams.scope !== "repository" && rawParams.scope !== "all") {
|
|
342
|
+
throw new Error("prepare-pattern-miner requires scope: repository or all.");
|
|
343
|
+
}
|
|
344
|
+
const limit = clamp(rawParams.limit, 1, 10, 10);
|
|
345
|
+
const inventory = await inventoryRepository(
|
|
346
|
+
pi,
|
|
347
|
+
{ cwd: ctx.cwd, signal },
|
|
348
|
+
rawParams.scope === "repository" ? "required" : "optional",
|
|
349
|
+
);
|
|
350
|
+
const sync = syncSessions(sessionsDir(), dbPath());
|
|
351
|
+
const currentSessionPath = ctx.sessionManager.getSessionFile() ?? undefined;
|
|
352
|
+
const rows = getPreparationRows(dbPath(), {
|
|
353
|
+
limit,
|
|
354
|
+
...(rawParams.scope === "repository" ? { repositoryRoot: inventory.gitRoot! } : {}),
|
|
355
|
+
currentSessionPath,
|
|
356
|
+
});
|
|
357
|
+
return textResult(buildPreparationResult(rawParams.scope, limit, inventory.gitRoot, sync, rows, inventory));
|
|
358
|
+
}
|
|
359
|
+
|
|
145
360
|
// LLMs sometimes send numeric ids/queries despite the string schema.
|
|
146
361
|
const params: ToolParams = {
|
|
147
362
|
query: rawParams.query != null ? String(rawParams.query) : undefined,
|
|
@@ -199,6 +414,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
199
414
|
(cap) => ({
|
|
200
415
|
mode: "read",
|
|
201
416
|
sessionId,
|
|
417
|
+
branchTip: r.branchTip,
|
|
202
418
|
totalMessages: r.totalMessages,
|
|
203
419
|
truncated: r.truncated,
|
|
204
420
|
messages: cap === null ? [] : truncateContent(r.messages, cap),
|
package/extensions/types.ts
CHANGED
|
@@ -22,6 +22,12 @@ export interface SessionRow {
|
|
|
22
22
|
preview?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/** Indexed metadata selected for pattern-miner preparation. */
|
|
26
|
+
export interface PreparationSessionRow extends SessionRow {
|
|
27
|
+
/** Stable one-hop fork identity used to avoid duplicate evidence. */
|
|
28
|
+
lineageId: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
/** One FTS discovery hit before hydration. */
|
|
26
32
|
export interface SearchHit {
|
|
27
33
|
path: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-session-recall",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "Local FTS5 search over past Pi sessions plus a skill for turning recurring work into deterministic automation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"LICENSE",
|
|
19
19
|
"extensions",
|
|
20
20
|
"skills",
|
|
21
|
+
"docs",
|
|
21
22
|
"README.md"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
@@ -5,22 +5,60 @@ description: Mine repeated work, corrections, and manual procedures from past Pi
|
|
|
5
5
|
|
|
6
6
|
# Pi Session Pattern Miner
|
|
7
7
|
|
|
8
|
-
Find repeated work in past Pi sessions
|
|
8
|
+
Find repeated work in past Pi sessions. Turn the best-supported pattern into the smallest useful automation.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
Use `session_search` for session history. Do not scan Pi session files directly.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
## Prepare once
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Choose the requested scope. Use `repository` for the current Git repository. Use `all` for a cross-repository request or work outside Git.
|
|
15
|
+
|
|
16
|
+
Before interpretation, call `session_search` exactly once with:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"operation": "prepare-pattern-miner",
|
|
21
|
+
"scope": "repository",
|
|
22
|
+
"limit": 10
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Change only `scope` when needed. Do not run manual browse, read, scroll, or inventory rounds during preparation.
|
|
27
|
+
|
|
28
|
+
State the returned scope and sample count. If `sync.complete` is false, report the incomplete walk or positive backlog as a sample limitation.
|
|
29
|
+
|
|
30
|
+
The prepared corpus is recent and bounded. A truncated session can omit middle episodes. Never claim exhaustive coverage.
|
|
15
31
|
|
|
16
32
|
## Mine
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
The model performs these steps. Do not replace them with mechanical keyword rules.
|
|
35
|
+
|
|
36
|
+
1. Identify semantic episodes, including repeated intents, corrections, retries, manual procedures, and recurring agent-authored steps.
|
|
37
|
+
2. Ignore generic inspect, edit, and test work unless the same concrete procedure repeats.
|
|
38
|
+
3. Cluster episodes by their underlying job, not their wording.
|
|
39
|
+
4. Treat sessions with equal `lineageId` values as one source.
|
|
40
|
+
5. Choose the owning automation surface from the prepared inventory and targeted repository evidence.
|
|
41
|
+
6. Write the recommendation and implementation contract.
|
|
42
|
+
|
|
43
|
+
A user-supplied topic is already a candidate. Run a focused confirmation search even when that topic is absent from the prepared sample.
|
|
44
|
+
|
|
45
|
+
For other work, create a candidate from the prepared corpus before running optional searches. Do not search speculatively when no topic or candidate exists.
|
|
46
|
+
|
|
47
|
+
Confirm a candidate with one or more distinctive `query` calls. Use `limit: 10`. Record the session path, date or name, relevant entry ID, and a short paraphrase.
|
|
48
|
+
|
|
49
|
+
Do not copy secrets or unnecessary transcript text. Do not count forks, retries, or continuations of one task as independent evidence.
|
|
50
|
+
|
|
51
|
+
## Verify ownership
|
|
52
|
+
|
|
53
|
+
After clustering, use the inventory only to find likely owners. It is not ownership proof and does not verify the current worktree.
|
|
54
|
+
|
|
55
|
+
Always inspect the current candidate-relevant package manifests, executable scripts, skill files, and instruction files before assigning ownership. Do this even when the inventory is available and complete.
|
|
56
|
+
|
|
57
|
+
Reuse or repair existing automation when it already owns the workflow. If targeted current-file verification cannot be performed safely, abstain.
|
|
58
|
+
|
|
59
|
+
## Evidence gate
|
|
60
|
+
|
|
61
|
+
A recommendation requires two independent examples. This gate is mandatory.
|
|
24
62
|
|
|
25
63
|
Stop with “not enough repeated evidence” when fewer than two independent sessions support a pattern. Do not manufacture a recommendation from one occurrence.
|
|
26
64
|
|
|
@@ -29,12 +67,14 @@ Stop with “not enough repeated evidence” when fewer than two independent ses
|
|
|
29
67
|
Stop at the first option that fully handles the pattern:
|
|
30
68
|
|
|
31
69
|
1. **Existing command or skill** — document, fix, or invoke it instead of adding another path.
|
|
32
|
-
2. **Script** —
|
|
33
|
-
3. **Script plus thin skill** —
|
|
34
|
-
4. **Skill only** —
|
|
35
|
-
5. **Product change** —
|
|
70
|
+
2. **Script** — use when inputs, decisions, outputs, and failures need no model judgment.
|
|
71
|
+
3. **Script plus thin skill** — use when an agent gathers inputs, but a script performs the repeated operation.
|
|
72
|
+
4. **Skill only** — use only when the reusable work inherently requires judgment, repository inspection, or user decisions.
|
|
73
|
+
5. **Product change** — use when the root cause belongs in an extension, API, CI check, or other code.
|
|
36
74
|
|
|
37
|
-
|
|
75
|
+
Prefer a repository command or standard-library script. Add no dependency without evidence that existing tools cannot handle the job.
|
|
76
|
+
|
|
77
|
+
A deterministic candidate must define its trigger, inputs, outputs, side effects, failure behavior, and one runnable check. Recommend investigation when evidence cannot define these details.
|
|
38
78
|
|
|
39
79
|
## Report
|
|
40
80
|
|
|
@@ -43,7 +83,9 @@ Return at most three ranked patterns:
|
|
|
43
83
|
| Pattern | Independent sessions | Repeated cost or failure | Existing coverage | Smallest automation |
|
|
44
84
|
| --- | ---: | --- | --- | --- |
|
|
45
85
|
|
|
46
|
-
For each pattern, cite
|
|
86
|
+
For each pattern, cite session paths and entry IDs. Explain why the owning surface fits and what model work it removes.
|
|
87
|
+
|
|
88
|
+
Give the highest-confidence candidate a minimal implementation contract:
|
|
47
89
|
|
|
48
90
|
- **Trigger and inputs**
|
|
49
91
|
- **Deterministic steps**
|