@aexol/spectral 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/relay/auto-research.js +247 -106
- package/package.json +1 -1
|
@@ -4,20 +4,50 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Flow:
|
|
6
6
|
* 1. Resolve project path from the SQLite store
|
|
7
|
-
* 2.
|
|
8
|
-
* 3.
|
|
9
|
-
* 4.
|
|
10
|
-
* 5.
|
|
11
|
-
* 6.
|
|
7
|
+
* 2. Load the auto-research agent definition (system prompt + model)
|
|
8
|
+
* 3. Write system prompt to a temp file (--append-system-prompt)
|
|
9
|
+
* 4. Spawn pi with --mode json -p --no-session --model <model> --append-system-prompt <tmp>
|
|
10
|
+
* 5. Pass the user task as a positional argument ("Task: ...")
|
|
11
|
+
* 6. Parse pi's JSON-line output: watch for message_end events on assistant
|
|
12
|
+
* messages, extract the text, and interpret it as auto-research events
|
|
13
|
+
* (progress / extension_generated / done / error)
|
|
14
|
+
* 7. Stream progress via the relay to the browser
|
|
15
|
+
* 8. On completion, emit `auto_research_complete` with generated extensions
|
|
12
16
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* file lives alongside other agent defs and gets loaded by pi's subagent
|
|
16
|
-
* infrastructure.
|
|
17
|
+
* This mirrors the subagent extension's spawn pattern (agent/index.ts) so
|
|
18
|
+
* pi receives the task and system prompt in the same format it expects.
|
|
17
19
|
*/
|
|
18
20
|
import { spawn } from "node:child_process";
|
|
19
21
|
import * as fs from "node:fs";
|
|
22
|
+
import * as os from "node:os";
|
|
20
23
|
import * as path from "node:path";
|
|
24
|
+
import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
25
|
+
/**
|
|
26
|
+
* Locate the auto-research agent definition markdown file.
|
|
27
|
+
* Searches project-level first, then user-level.
|
|
28
|
+
*/
|
|
29
|
+
function findAgentDef(projectPath) {
|
|
30
|
+
const candidates = [
|
|
31
|
+
path.join(projectPath, ".pi", "agents", "auto-research.md"),
|
|
32
|
+
path.join(os.homedir(), ".pi", "agent", "agents", "auto-research.md"),
|
|
33
|
+
];
|
|
34
|
+
for (const filePath of candidates) {
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
37
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
38
|
+
if (body.trim().length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
return {
|
|
41
|
+
model: frontmatter.model ?? "claude-sonnet-4-5",
|
|
42
|
+
systemPrompt: body,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
21
51
|
// ---------------------------------------------------------------------------
|
|
22
52
|
// Helpers
|
|
23
53
|
// ---------------------------------------------------------------------------
|
|
@@ -77,7 +107,6 @@ function killProcess(child) {
|
|
|
77
107
|
try {
|
|
78
108
|
if (!child.killed && child.exitCode === null) {
|
|
79
109
|
child.kill("SIGTERM");
|
|
80
|
-
// Give it 2 seconds to clean up, then force-kill
|
|
81
110
|
setTimeout(() => {
|
|
82
111
|
try {
|
|
83
112
|
if (!child.killed && child.exitCode === null) {
|
|
@@ -95,6 +124,21 @@ function killProcess(child) {
|
|
|
95
124
|
}
|
|
96
125
|
}
|
|
97
126
|
// ---------------------------------------------------------------------------
|
|
127
|
+
// Phase mapping
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
const VALID_PHASES = new Set([
|
|
130
|
+
"context_collecting",
|
|
131
|
+
"context_analyzing",
|
|
132
|
+
"extension_generating",
|
|
133
|
+
"extension_validating",
|
|
134
|
+
]);
|
|
135
|
+
function mapPhase(agentPhase) {
|
|
136
|
+
if (VALID_PHASES.has(agentPhase)) {
|
|
137
|
+
return agentPhase;
|
|
138
|
+
}
|
|
139
|
+
return "context_analyzing";
|
|
140
|
+
}
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
98
142
|
// Handler
|
|
99
143
|
// ---------------------------------------------------------------------------
|
|
100
144
|
const AR_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — generous for LLM-based analysis
|
|
@@ -107,7 +151,7 @@ const AR_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — generous for LLM-based ana
|
|
|
107
151
|
*/
|
|
108
152
|
export function handleAutoResearch(input, deps) {
|
|
109
153
|
const { projectId, sessionId } = input;
|
|
110
|
-
const { store
|
|
154
|
+
const { store } = deps;
|
|
111
155
|
const logger = deps.logger ?? console;
|
|
112
156
|
// 1. Resolve the project from the store.
|
|
113
157
|
const project = store.getProject(projectId);
|
|
@@ -120,25 +164,75 @@ export function handleAutoResearch(input, deps) {
|
|
|
120
164
|
return;
|
|
121
165
|
}
|
|
122
166
|
const projectPath = project.path;
|
|
123
|
-
// 2.
|
|
167
|
+
// 2. Load the auto-research agent definition.
|
|
168
|
+
const agentDef = findAgentDef(projectPath);
|
|
169
|
+
if (!agentDef) {
|
|
170
|
+
sendEvent(deps, sessionId, {
|
|
171
|
+
type: "auto_research_error",
|
|
172
|
+
projectId,
|
|
173
|
+
message: "Auto-research agent definition not found. " +
|
|
174
|
+
"Create .pi/agents/auto-research.md in the project or ~/.pi/agent/agents/auto-research.md.",
|
|
175
|
+
});
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// 3. Emit start event
|
|
124
179
|
sendEvent(deps, sessionId, {
|
|
125
180
|
type: "auto_research_start",
|
|
126
181
|
projectId,
|
|
127
182
|
});
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
// 4. Write the system prompt to a temp file so pi can load it via
|
|
184
|
+
// --append-system-prompt (mirrors the subagent extension pattern).
|
|
185
|
+
let tmpDir = null;
|
|
186
|
+
let tmpPromptPath = null;
|
|
187
|
+
try {
|
|
188
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "spectral-auto-research-"));
|
|
189
|
+
tmpPromptPath = path.join(tmpDir, "system-prompt.md");
|
|
190
|
+
fs.writeFileSync(tmpPromptPath, agentDef.systemPrompt, { encoding: "utf-8", mode: 0o600 });
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
194
|
+
sendEvent(deps, sessionId, {
|
|
195
|
+
type: "auto_research_error",
|
|
196
|
+
projectId,
|
|
197
|
+
message: `Failed to write system prompt temp file: ${msg}`,
|
|
198
|
+
});
|
|
199
|
+
if (tmpDir) {
|
|
200
|
+
try {
|
|
201
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
catch { /* ignore */ }
|
|
204
|
+
}
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
// 5. Build the user task. This becomes a positional argument to pi,
|
|
208
|
+
// matching how the subagent extension passes tasks.
|
|
209
|
+
const task = buildUserTask(projectPath, project.name);
|
|
210
|
+
// 6. Build spawn arguments (mirroring agent/index.ts subprocess spawn).
|
|
211
|
+
const args = [
|
|
212
|
+
"--mode", "json",
|
|
213
|
+
"-p",
|
|
214
|
+
"--no-session",
|
|
215
|
+
"--model", agentDef.model,
|
|
216
|
+
"--append-system-prompt", tmpPromptPath,
|
|
217
|
+
];
|
|
218
|
+
// Add tools if defined in the agent definition. The auto-research agent
|
|
219
|
+
// uses read/grep/find/ls/bash/write/edit — its frontmatter should declare
|
|
220
|
+
// them so pi allows those tools.
|
|
221
|
+
// We don't extract tools from frontmatter currently because parseFrontmatter
|
|
222
|
+
// returns a generic Record. For now, auto-research always uses the default
|
|
223
|
+
// tool set (pi's full tool set is available by default in -p mode).
|
|
224
|
+
// TODO: when agent def frontmatter parsing is unified with agents.ts, also
|
|
225
|
+
// pass --tools here.
|
|
226
|
+
// The user task is the last positional argument — pi treats it as the
|
|
227
|
+
// initial prompt in -p mode.
|
|
228
|
+
args.push(`Task: ${task}`);
|
|
229
|
+
// 7. Spawn pi subprocess.
|
|
136
230
|
const invocation = getPiInvocation(args);
|
|
137
231
|
let child;
|
|
138
232
|
try {
|
|
139
233
|
child = spawn(invocation.command, invocation.args, {
|
|
140
234
|
cwd: projectPath,
|
|
141
|
-
stdio: ["
|
|
235
|
+
stdio: ["ignore", "pipe", "pipe"], // stdin ignored — task is positional
|
|
142
236
|
env: { ...process.env },
|
|
143
237
|
shell: false,
|
|
144
238
|
});
|
|
@@ -150,9 +244,10 @@ export function handleAutoResearch(input, deps) {
|
|
|
150
244
|
projectId,
|
|
151
245
|
message: `Failed to spawn pi subprocess: ${msg}`,
|
|
152
246
|
});
|
|
247
|
+
cleanupTemp(tmpDir);
|
|
153
248
|
return;
|
|
154
249
|
}
|
|
155
|
-
//
|
|
250
|
+
// 8. Set up timeout
|
|
156
251
|
const timeout = setTimeout(() => {
|
|
157
252
|
killProcess(child);
|
|
158
253
|
sendEvent(deps, sessionId, {
|
|
@@ -160,8 +255,18 @@ export function handleAutoResearch(input, deps) {
|
|
|
160
255
|
projectId,
|
|
161
256
|
message: "Auto-research timed out after 5 minutes",
|
|
162
257
|
});
|
|
258
|
+
cleanupTemp(tmpDir);
|
|
163
259
|
}, AR_TIMEOUT_MS);
|
|
164
|
-
//
|
|
260
|
+
// 9. Collect stdout and parse pi's JSON-line output format.
|
|
261
|
+
// pi in --mode json emits one JSON line per event:
|
|
262
|
+
// {"type":"message_start",...}
|
|
263
|
+
// {"type":"text_delta","content":"..."}
|
|
264
|
+
// {"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"..."}]}}
|
|
265
|
+
// {"type":"agent_end",...}
|
|
266
|
+
//
|
|
267
|
+
// The auto-research agent's output is inside assistant message_end
|
|
268
|
+
// events. We extract the text and try to parse it as auto-research
|
|
269
|
+
// event JSON (progress, extension_generated, done, error).
|
|
165
270
|
let stdoutBuffer = "";
|
|
166
271
|
const discoveredExtensions = [];
|
|
167
272
|
let stderrBuffer = "";
|
|
@@ -169,34 +274,102 @@ export function handleAutoResearch(input, deps) {
|
|
|
169
274
|
stdoutBuffer += chunk.toString("utf-8");
|
|
170
275
|
// Process complete lines
|
|
171
276
|
const lines = stdoutBuffer.split("\n");
|
|
172
|
-
// The last element may be an incomplete line — keep it in the buffer
|
|
173
277
|
stdoutBuffer = lines.pop() ?? "";
|
|
174
278
|
for (const rawLine of lines) {
|
|
175
279
|
const line = rawLine.trim();
|
|
176
280
|
if (!line)
|
|
177
281
|
continue;
|
|
178
|
-
let
|
|
282
|
+
let event;
|
|
179
283
|
try {
|
|
180
|
-
|
|
284
|
+
event = JSON.parse(line);
|
|
181
285
|
}
|
|
182
286
|
catch {
|
|
183
|
-
// Non-JSON output —
|
|
287
|
+
// Non-JSON output — ignore.
|
|
184
288
|
continue;
|
|
185
289
|
}
|
|
186
|
-
|
|
290
|
+
// Only process message_end events from the assistant.
|
|
291
|
+
if (event.type !== "message_end" || !event.message)
|
|
292
|
+
continue;
|
|
293
|
+
if (event.message.role !== "assistant")
|
|
294
|
+
continue;
|
|
295
|
+
const content = event.message.content;
|
|
296
|
+
if (!content || !Array.isArray(content) || content.length === 0)
|
|
297
|
+
continue;
|
|
298
|
+
// Extract text blocks from the assistant message
|
|
299
|
+
for (const block of content) {
|
|
300
|
+
if (block.type !== "text" || typeof block.text !== "string")
|
|
301
|
+
continue;
|
|
302
|
+
// Try to parse the assistant's text output as one or more JSON
|
|
303
|
+
// auto-research events. The agent may output multiple JSON objects
|
|
304
|
+
// in a single assistant message (separated by newlines or
|
|
305
|
+
// concatenated). We try parsing the full text first, then fall
|
|
306
|
+
// back to line-by-line.
|
|
307
|
+
const text = block.text.trim();
|
|
308
|
+
// First, try treating the entire text block as a single event
|
|
309
|
+
let parsed = null;
|
|
310
|
+
try {
|
|
311
|
+
parsed = JSON.parse(text);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// Not a single JSON object — try line-by-line
|
|
315
|
+
}
|
|
316
|
+
if (parsed && parsed.type) {
|
|
317
|
+
processArEvent(parsed, discoveredExtensions, projectId, sessionId, deps);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// Try each line individually (the agent may emit multi-line
|
|
321
|
+
// JSON event output, e.g. one event per line).
|
|
322
|
+
for (const subLine of text.split("\n")) {
|
|
323
|
+
const trimmed = subLine.trim();
|
|
324
|
+
if (!trimmed)
|
|
325
|
+
continue;
|
|
326
|
+
try {
|
|
327
|
+
const eventLine = JSON.parse(trimmed);
|
|
328
|
+
if (eventLine && eventLine.type) {
|
|
329
|
+
processArEvent(eventLine, discoveredExtensions, projectId, sessionId, deps);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
// skip non-JSON lines
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
187
338
|
}
|
|
188
339
|
});
|
|
189
340
|
child.stderr?.on("data", (chunk) => {
|
|
190
341
|
stderrBuffer += chunk.toString("utf-8");
|
|
191
342
|
});
|
|
192
|
-
//
|
|
343
|
+
// 10. Handle process exit
|
|
193
344
|
child.on("close", (code) => {
|
|
194
345
|
clearTimeout(timeout);
|
|
195
|
-
// Process any remaining buffered stdout
|
|
346
|
+
// Process any remaining buffered stdout (drain the buffer)
|
|
196
347
|
if (stdoutBuffer.trim()) {
|
|
197
348
|
try {
|
|
198
|
-
const
|
|
199
|
-
|
|
349
|
+
const event = JSON.parse(stdoutBuffer.trim());
|
|
350
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
351
|
+
const content = event.message?.content;
|
|
352
|
+
if (Array.isArray(content)) {
|
|
353
|
+
for (const block of content) {
|
|
354
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
355
|
+
for (const subLine of block.text.split("\n")) {
|
|
356
|
+
const trimmed = subLine.trim();
|
|
357
|
+
if (!trimmed)
|
|
358
|
+
continue;
|
|
359
|
+
try {
|
|
360
|
+
const ar = JSON.parse(trimmed);
|
|
361
|
+
if (ar && ar.type) {
|
|
362
|
+
processArEvent(ar, discoveredExtensions, projectId, sessionId, deps);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
/* skip */
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
200
373
|
}
|
|
201
374
|
catch {
|
|
202
375
|
// ignore
|
|
@@ -212,6 +385,7 @@ export function handleAutoResearch(input, deps) {
|
|
|
212
385
|
projectId,
|
|
213
386
|
message: `Auto-research subprocess exited with code ${code}${errDetail}`,
|
|
214
387
|
});
|
|
388
|
+
cleanupTemp(tmpDir);
|
|
215
389
|
return;
|
|
216
390
|
}
|
|
217
391
|
// Emit completion with any discovered extensions (even partial — the
|
|
@@ -225,6 +399,7 @@ export function handleAutoResearch(input, deps) {
|
|
|
225
399
|
if (stderrBuffer) {
|
|
226
400
|
logger.error?.(`[auto-research] subprocess stderr (code=${code}): ${stderrBuffer.slice(0, 1000)}`);
|
|
227
401
|
}
|
|
402
|
+
cleanupTemp(tmpDir);
|
|
228
403
|
});
|
|
229
404
|
child.on("error", (err) => {
|
|
230
405
|
clearTimeout(timeout);
|
|
@@ -233,15 +408,13 @@ export function handleAutoResearch(input, deps) {
|
|
|
233
408
|
projectId,
|
|
234
409
|
message: `Auto-research subprocess error: ${err.message}`,
|
|
235
410
|
});
|
|
411
|
+
cleanupTemp(tmpDir);
|
|
236
412
|
});
|
|
237
|
-
// 8. Write the task to stdin and close
|
|
238
|
-
child.stdin?.write(task);
|
|
239
|
-
child.stdin?.end();
|
|
240
413
|
}
|
|
241
414
|
// ---------------------------------------------------------------------------
|
|
242
|
-
//
|
|
415
|
+
// Event processor
|
|
243
416
|
// ---------------------------------------------------------------------------
|
|
244
|
-
function
|
|
417
|
+
function processArEvent(parsed, extensions, projectId, sessionId, deps) {
|
|
245
418
|
const t = parsed.type;
|
|
246
419
|
if (t === "progress") {
|
|
247
420
|
const p = parsed;
|
|
@@ -262,6 +435,13 @@ function processArLine(parsed, extensions, projectId, sessionId, deps) {
|
|
|
262
435
|
usesLLM: eg.usesLLM,
|
|
263
436
|
fileCount: eg.fileCount,
|
|
264
437
|
});
|
|
438
|
+
// Also emit as a progress update so the UI shows real-time activity
|
|
439
|
+
sendEvent(deps, sessionId, {
|
|
440
|
+
type: "auto_research_progress",
|
|
441
|
+
projectId,
|
|
442
|
+
phase: "extension_generating",
|
|
443
|
+
message: `Generated: ${eg.name}`,
|
|
444
|
+
});
|
|
265
445
|
}
|
|
266
446
|
else if (t === "done") {
|
|
267
447
|
const d = parsed;
|
|
@@ -281,77 +461,38 @@ function processArLine(parsed, extensions, projectId, sessionId, deps) {
|
|
|
281
461
|
// Unknown event types are silently ignored for forward compatibility
|
|
282
462
|
}
|
|
283
463
|
// ---------------------------------------------------------------------------
|
|
284
|
-
// Phase mapping
|
|
285
|
-
// ---------------------------------------------------------------------------
|
|
286
|
-
const VALID_PHASES = new Set([
|
|
287
|
-
"context_collecting",
|
|
288
|
-
"context_analyzing",
|
|
289
|
-
"extension_generating",
|
|
290
|
-
"extension_validating",
|
|
291
|
-
]);
|
|
292
|
-
function mapPhase(agentPhase) {
|
|
293
|
-
if (VALID_PHASES.has(agentPhase)) {
|
|
294
|
-
return agentPhase;
|
|
295
|
-
}
|
|
296
|
-
return "context_analyzing";
|
|
297
|
-
}
|
|
298
|
-
// ---------------------------------------------------------------------------
|
|
299
464
|
// Task builder
|
|
300
465
|
// ---------------------------------------------------------------------------
|
|
301
466
|
/**
|
|
302
|
-
* Build the
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* 1. Collect context about the project
|
|
306
|
-
* 2. Analyze what extensions would be useful
|
|
307
|
-
* 3. Generate extension definitions
|
|
308
|
-
* 4. Report results as structured JSON lines
|
|
467
|
+
* Build the user task prompt sent as positional argument to pi.
|
|
468
|
+
* The system prompt (from the agent definition) provides the detailed
|
|
469
|
+
* instructions; this is just the project-specific context.
|
|
309
470
|
*/
|
|
310
|
-
function
|
|
311
|
-
return
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
Extension categories to consider:
|
|
338
|
-
A. Workflow automation (test runner, linter integration, git hooks)
|
|
339
|
-
B. Code generation (component scaffolding, API route generators)
|
|
340
|
-
C. Project-specific tools (database migrations, schema generators)
|
|
341
|
-
D. Quality & review (code reviewer, error explainer, type checker)
|
|
342
|
-
E. Documentation (doc generator, changelog, API reference)
|
|
343
|
-
F. LLM-powered (commit message generator, PR description, architecture advisor)
|
|
344
|
-
G. Stateful (session memory extensions, usage dashboard, adaptive guidelines)
|
|
345
|
-
|
|
346
|
-
4. **Validation** (emit progress: phase="extension_validating")
|
|
347
|
-
- Verify generated extensions follow pi extension API patterns
|
|
348
|
-
- Ensure imports resolve against available npm packages
|
|
349
|
-
|
|
350
|
-
5. **Completion**
|
|
351
|
-
When done, emit:
|
|
352
|
-
{"type":"done","extensions":[...]}
|
|
353
|
-
Then exit.
|
|
354
|
-
|
|
355
|
-
IMPORTANT: Output ONLY JSON lines (one per line). Do not emit markdown, explanations, or any other text outside of JSON. Each line must be valid JSON on a single line. Your output will be parsed by a machine, not a human.`,
|
|
356
|
-
});
|
|
471
|
+
function buildUserTask(projectPath, projectName) {
|
|
472
|
+
return [
|
|
473
|
+
`Analyze the project at "${projectPath}" named "${projectName}" to determine`,
|
|
474
|
+
`what custom pi coding agent extensions would accelerate development.`,
|
|
475
|
+
``,
|
|
476
|
+
`Follow your system prompt instructions for the full process:`,
|
|
477
|
+
`1. Context collection — scan the project structure`,
|
|
478
|
+
`2. Analysis — identify patterns and automation opportunities`,
|
|
479
|
+
`3. Extension generation — create .ts extension files`,
|
|
480
|
+
`4. Validation — verify the extensions are correct`,
|
|
481
|
+
``,
|
|
482
|
+
`Important: Read the template library at .pi/agents/auto-research-templates.md`,
|
|
483
|
+
`if it exists, for ready-to-adapt extension templates.`,
|
|
484
|
+
].join("\n");
|
|
485
|
+
}
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Temp file cleanup
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
function cleanupTemp(tmpDir) {
|
|
490
|
+
if (!tmpDir)
|
|
491
|
+
return;
|
|
492
|
+
try {
|
|
493
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// best-effort cleanup
|
|
497
|
+
}
|
|
357
498
|
}
|