@aexol/spectral 0.6.2 → 0.6.4
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 +279 -106
- package/package.json +1 -1
|
@@ -4,20 +4,82 @@
|
|
|
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
|
+
// Hardcoded fallback: system prompt for when the agent definition file
|
|
50
|
+
// is not found in the project or user agent directories. This ensures
|
|
51
|
+
// auto-research works out of the box on first use.
|
|
52
|
+
return getDefaultAgentDef();
|
|
53
|
+
}
|
|
54
|
+
/** Default agent definition used as a fallback when no agent .md file exists. */
|
|
55
|
+
function getDefaultAgentDef() {
|
|
56
|
+
return {
|
|
57
|
+
model: "claude-sonnet-4-5",
|
|
58
|
+
systemPrompt: [
|
|
59
|
+
"You are an auto-research agent. Analyze the project and generate custom",
|
|
60
|
+
"pi coding agent extensions. Output ONLY JSON lines (one per line).",
|
|
61
|
+
"",
|
|
62
|
+
"## Process",
|
|
63
|
+
'1. Context: emit {"type":"progress","phase":"context_collecting","message":"..."}',
|
|
64
|
+
'2. Analysis: emit {"type":"progress","phase":"context_analyzing","message":"..."}',
|
|
65
|
+
'3. Generation: emit {"type":"progress","phase":"extension_generating","message":"..."}',
|
|
66
|
+
'4. Validation: emit {"type":"progress","phase":"extension_validating","message":"..."}',
|
|
67
|
+
"",
|
|
68
|
+
"## Extension categories",
|
|
69
|
+
"A. Workflow automation B. Code gen C. Project-specific tools",
|
|
70
|
+
"D. Quality/review E. Documentation F. LLM-powered G. Stateful",
|
|
71
|
+
"",
|
|
72
|
+
'When you generate an extension, emit:',
|
|
73
|
+
'{"type":"extension_generated","name":"...","path":"...","description":"...","usesLLM":bool,"fileCount":n}',
|
|
74
|
+
"Extensions go under .pi/extensions/auto-research/",
|
|
75
|
+
"",
|
|
76
|
+
'When done, emit: {"type":"done","extensions":[...]}',
|
|
77
|
+
"",
|
|
78
|
+
"IMPORTANT: Output ONLY JSON lines. No markdown, no code blocks.",
|
|
79
|
+
"Each line must be a single valid JSON object.",
|
|
80
|
+
].join("\n"),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
21
83
|
// ---------------------------------------------------------------------------
|
|
22
84
|
// Helpers
|
|
23
85
|
// ---------------------------------------------------------------------------
|
|
@@ -77,7 +139,6 @@ function killProcess(child) {
|
|
|
77
139
|
try {
|
|
78
140
|
if (!child.killed && child.exitCode === null) {
|
|
79
141
|
child.kill("SIGTERM");
|
|
80
|
-
// Give it 2 seconds to clean up, then force-kill
|
|
81
142
|
setTimeout(() => {
|
|
82
143
|
try {
|
|
83
144
|
if (!child.killed && child.exitCode === null) {
|
|
@@ -95,6 +156,21 @@ function killProcess(child) {
|
|
|
95
156
|
}
|
|
96
157
|
}
|
|
97
158
|
// ---------------------------------------------------------------------------
|
|
159
|
+
// Phase mapping
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
const VALID_PHASES = new Set([
|
|
162
|
+
"context_collecting",
|
|
163
|
+
"context_analyzing",
|
|
164
|
+
"extension_generating",
|
|
165
|
+
"extension_validating",
|
|
166
|
+
]);
|
|
167
|
+
function mapPhase(agentPhase) {
|
|
168
|
+
if (VALID_PHASES.has(agentPhase)) {
|
|
169
|
+
return agentPhase;
|
|
170
|
+
}
|
|
171
|
+
return "context_analyzing";
|
|
172
|
+
}
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
98
174
|
// Handler
|
|
99
175
|
// ---------------------------------------------------------------------------
|
|
100
176
|
const AR_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — generous for LLM-based analysis
|
|
@@ -107,7 +183,7 @@ const AR_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes — generous for LLM-based ana
|
|
|
107
183
|
*/
|
|
108
184
|
export function handleAutoResearch(input, deps) {
|
|
109
185
|
const { projectId, sessionId } = input;
|
|
110
|
-
const { store
|
|
186
|
+
const { store } = deps;
|
|
111
187
|
const logger = deps.logger ?? console;
|
|
112
188
|
// 1. Resolve the project from the store.
|
|
113
189
|
const project = store.getProject(projectId);
|
|
@@ -120,25 +196,75 @@ export function handleAutoResearch(input, deps) {
|
|
|
120
196
|
return;
|
|
121
197
|
}
|
|
122
198
|
const projectPath = project.path;
|
|
123
|
-
// 2.
|
|
199
|
+
// 2. Load the auto-research agent definition.
|
|
200
|
+
const agentDef = findAgentDef(projectPath);
|
|
201
|
+
if (!agentDef) {
|
|
202
|
+
sendEvent(deps, sessionId, {
|
|
203
|
+
type: "auto_research_error",
|
|
204
|
+
projectId,
|
|
205
|
+
message: "Auto-research agent definition not found. " +
|
|
206
|
+
"Create .pi/agents/auto-research.md in the project or ~/.pi/agent/agents/auto-research.md.",
|
|
207
|
+
});
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// 3. Emit start event
|
|
124
211
|
sendEvent(deps, sessionId, {
|
|
125
212
|
type: "auto_research_start",
|
|
126
213
|
projectId,
|
|
127
214
|
});
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
215
|
+
// 4. Write the system prompt to a temp file so pi can load it via
|
|
216
|
+
// --append-system-prompt (mirrors the subagent extension pattern).
|
|
217
|
+
let tmpDir = null;
|
|
218
|
+
let tmpPromptPath = null;
|
|
219
|
+
try {
|
|
220
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "spectral-auto-research-"));
|
|
221
|
+
tmpPromptPath = path.join(tmpDir, "system-prompt.md");
|
|
222
|
+
fs.writeFileSync(tmpPromptPath, agentDef.systemPrompt, { encoding: "utf-8", mode: 0o600 });
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
226
|
+
sendEvent(deps, sessionId, {
|
|
227
|
+
type: "auto_research_error",
|
|
228
|
+
projectId,
|
|
229
|
+
message: `Failed to write system prompt temp file: ${msg}`,
|
|
230
|
+
});
|
|
231
|
+
if (tmpDir) {
|
|
232
|
+
try {
|
|
233
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
catch { /* ignore */ }
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// 5. Build the user task. This becomes a positional argument to pi,
|
|
240
|
+
// matching how the subagent extension passes tasks.
|
|
241
|
+
const task = buildUserTask(projectPath, project.name);
|
|
242
|
+
// 6. Build spawn arguments (mirroring agent/index.ts subprocess spawn).
|
|
243
|
+
const args = [
|
|
244
|
+
"--mode", "json",
|
|
245
|
+
"-p",
|
|
246
|
+
"--no-session",
|
|
247
|
+
"--model", agentDef.model,
|
|
248
|
+
"--append-system-prompt", tmpPromptPath,
|
|
249
|
+
];
|
|
250
|
+
// Add tools if defined in the agent definition. The auto-research agent
|
|
251
|
+
// uses read/grep/find/ls/bash/write/edit — its frontmatter should declare
|
|
252
|
+
// them so pi allows those tools.
|
|
253
|
+
// We don't extract tools from frontmatter currently because parseFrontmatter
|
|
254
|
+
// returns a generic Record. For now, auto-research always uses the default
|
|
255
|
+
// tool set (pi's full tool set is available by default in -p mode).
|
|
256
|
+
// TODO: when agent def frontmatter parsing is unified with agents.ts, also
|
|
257
|
+
// pass --tools here.
|
|
258
|
+
// The user task is the last positional argument — pi treats it as the
|
|
259
|
+
// initial prompt in -p mode.
|
|
260
|
+
args.push(`Task: ${task}`);
|
|
261
|
+
// 7. Spawn pi subprocess.
|
|
136
262
|
const invocation = getPiInvocation(args);
|
|
137
263
|
let child;
|
|
138
264
|
try {
|
|
139
265
|
child = spawn(invocation.command, invocation.args, {
|
|
140
266
|
cwd: projectPath,
|
|
141
|
-
stdio: ["
|
|
267
|
+
stdio: ["ignore", "pipe", "pipe"], // stdin ignored — task is positional
|
|
142
268
|
env: { ...process.env },
|
|
143
269
|
shell: false,
|
|
144
270
|
});
|
|
@@ -150,9 +276,10 @@ export function handleAutoResearch(input, deps) {
|
|
|
150
276
|
projectId,
|
|
151
277
|
message: `Failed to spawn pi subprocess: ${msg}`,
|
|
152
278
|
});
|
|
279
|
+
cleanupTemp(tmpDir);
|
|
153
280
|
return;
|
|
154
281
|
}
|
|
155
|
-
//
|
|
282
|
+
// 8. Set up timeout
|
|
156
283
|
const timeout = setTimeout(() => {
|
|
157
284
|
killProcess(child);
|
|
158
285
|
sendEvent(deps, sessionId, {
|
|
@@ -160,8 +287,18 @@ export function handleAutoResearch(input, deps) {
|
|
|
160
287
|
projectId,
|
|
161
288
|
message: "Auto-research timed out after 5 minutes",
|
|
162
289
|
});
|
|
290
|
+
cleanupTemp(tmpDir);
|
|
163
291
|
}, AR_TIMEOUT_MS);
|
|
164
|
-
//
|
|
292
|
+
// 9. Collect stdout and parse pi's JSON-line output format.
|
|
293
|
+
// pi in --mode json emits one JSON line per event:
|
|
294
|
+
// {"type":"message_start",...}
|
|
295
|
+
// {"type":"text_delta","content":"..."}
|
|
296
|
+
// {"type":"message_end","message":{"role":"assistant","content":[{"type":"text","text":"..."}]}}
|
|
297
|
+
// {"type":"agent_end",...}
|
|
298
|
+
//
|
|
299
|
+
// The auto-research agent's output is inside assistant message_end
|
|
300
|
+
// events. We extract the text and try to parse it as auto-research
|
|
301
|
+
// event JSON (progress, extension_generated, done, error).
|
|
165
302
|
let stdoutBuffer = "";
|
|
166
303
|
const discoveredExtensions = [];
|
|
167
304
|
let stderrBuffer = "";
|
|
@@ -169,34 +306,102 @@ export function handleAutoResearch(input, deps) {
|
|
|
169
306
|
stdoutBuffer += chunk.toString("utf-8");
|
|
170
307
|
// Process complete lines
|
|
171
308
|
const lines = stdoutBuffer.split("\n");
|
|
172
|
-
// The last element may be an incomplete line — keep it in the buffer
|
|
173
309
|
stdoutBuffer = lines.pop() ?? "";
|
|
174
310
|
for (const rawLine of lines) {
|
|
175
311
|
const line = rawLine.trim();
|
|
176
312
|
if (!line)
|
|
177
313
|
continue;
|
|
178
|
-
let
|
|
314
|
+
let event;
|
|
179
315
|
try {
|
|
180
|
-
|
|
316
|
+
event = JSON.parse(line);
|
|
181
317
|
}
|
|
182
318
|
catch {
|
|
183
|
-
// Non-JSON output —
|
|
319
|
+
// Non-JSON output — ignore.
|
|
184
320
|
continue;
|
|
185
321
|
}
|
|
186
|
-
|
|
322
|
+
// Only process message_end events from the assistant.
|
|
323
|
+
if (event.type !== "message_end" || !event.message)
|
|
324
|
+
continue;
|
|
325
|
+
if (event.message.role !== "assistant")
|
|
326
|
+
continue;
|
|
327
|
+
const content = event.message.content;
|
|
328
|
+
if (!content || !Array.isArray(content) || content.length === 0)
|
|
329
|
+
continue;
|
|
330
|
+
// Extract text blocks from the assistant message
|
|
331
|
+
for (const block of content) {
|
|
332
|
+
if (block.type !== "text" || typeof block.text !== "string")
|
|
333
|
+
continue;
|
|
334
|
+
// Try to parse the assistant's text output as one or more JSON
|
|
335
|
+
// auto-research events. The agent may output multiple JSON objects
|
|
336
|
+
// in a single assistant message (separated by newlines or
|
|
337
|
+
// concatenated). We try parsing the full text first, then fall
|
|
338
|
+
// back to line-by-line.
|
|
339
|
+
const text = block.text.trim();
|
|
340
|
+
// First, try treating the entire text block as a single event
|
|
341
|
+
let parsed = null;
|
|
342
|
+
try {
|
|
343
|
+
parsed = JSON.parse(text);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
// Not a single JSON object — try line-by-line
|
|
347
|
+
}
|
|
348
|
+
if (parsed && parsed.type) {
|
|
349
|
+
processArEvent(parsed, discoveredExtensions, projectId, sessionId, deps);
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
// Try each line individually (the agent may emit multi-line
|
|
353
|
+
// JSON event output, e.g. one event per line).
|
|
354
|
+
for (const subLine of text.split("\n")) {
|
|
355
|
+
const trimmed = subLine.trim();
|
|
356
|
+
if (!trimmed)
|
|
357
|
+
continue;
|
|
358
|
+
try {
|
|
359
|
+
const eventLine = JSON.parse(trimmed);
|
|
360
|
+
if (eventLine && eventLine.type) {
|
|
361
|
+
processArEvent(eventLine, discoveredExtensions, projectId, sessionId, deps);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// skip non-JSON lines
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
187
370
|
}
|
|
188
371
|
});
|
|
189
372
|
child.stderr?.on("data", (chunk) => {
|
|
190
373
|
stderrBuffer += chunk.toString("utf-8");
|
|
191
374
|
});
|
|
192
|
-
//
|
|
375
|
+
// 10. Handle process exit
|
|
193
376
|
child.on("close", (code) => {
|
|
194
377
|
clearTimeout(timeout);
|
|
195
|
-
// Process any remaining buffered stdout
|
|
378
|
+
// Process any remaining buffered stdout (drain the buffer)
|
|
196
379
|
if (stdoutBuffer.trim()) {
|
|
197
380
|
try {
|
|
198
|
-
const
|
|
199
|
-
|
|
381
|
+
const event = JSON.parse(stdoutBuffer.trim());
|
|
382
|
+
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
383
|
+
const content = event.message?.content;
|
|
384
|
+
if (Array.isArray(content)) {
|
|
385
|
+
for (const block of content) {
|
|
386
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
387
|
+
for (const subLine of block.text.split("\n")) {
|
|
388
|
+
const trimmed = subLine.trim();
|
|
389
|
+
if (!trimmed)
|
|
390
|
+
continue;
|
|
391
|
+
try {
|
|
392
|
+
const ar = JSON.parse(trimmed);
|
|
393
|
+
if (ar && ar.type) {
|
|
394
|
+
processArEvent(ar, discoveredExtensions, projectId, sessionId, deps);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
/* skip */
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
200
405
|
}
|
|
201
406
|
catch {
|
|
202
407
|
// ignore
|
|
@@ -212,6 +417,7 @@ export function handleAutoResearch(input, deps) {
|
|
|
212
417
|
projectId,
|
|
213
418
|
message: `Auto-research subprocess exited with code ${code}${errDetail}`,
|
|
214
419
|
});
|
|
420
|
+
cleanupTemp(tmpDir);
|
|
215
421
|
return;
|
|
216
422
|
}
|
|
217
423
|
// Emit completion with any discovered extensions (even partial — the
|
|
@@ -225,6 +431,7 @@ export function handleAutoResearch(input, deps) {
|
|
|
225
431
|
if (stderrBuffer) {
|
|
226
432
|
logger.error?.(`[auto-research] subprocess stderr (code=${code}): ${stderrBuffer.slice(0, 1000)}`);
|
|
227
433
|
}
|
|
434
|
+
cleanupTemp(tmpDir);
|
|
228
435
|
});
|
|
229
436
|
child.on("error", (err) => {
|
|
230
437
|
clearTimeout(timeout);
|
|
@@ -233,15 +440,13 @@ export function handleAutoResearch(input, deps) {
|
|
|
233
440
|
projectId,
|
|
234
441
|
message: `Auto-research subprocess error: ${err.message}`,
|
|
235
442
|
});
|
|
443
|
+
cleanupTemp(tmpDir);
|
|
236
444
|
});
|
|
237
|
-
// 8. Write the task to stdin and close
|
|
238
|
-
child.stdin?.write(task);
|
|
239
|
-
child.stdin?.end();
|
|
240
445
|
}
|
|
241
446
|
// ---------------------------------------------------------------------------
|
|
242
|
-
//
|
|
447
|
+
// Event processor
|
|
243
448
|
// ---------------------------------------------------------------------------
|
|
244
|
-
function
|
|
449
|
+
function processArEvent(parsed, extensions, projectId, sessionId, deps) {
|
|
245
450
|
const t = parsed.type;
|
|
246
451
|
if (t === "progress") {
|
|
247
452
|
const p = parsed;
|
|
@@ -262,6 +467,13 @@ function processArLine(parsed, extensions, projectId, sessionId, deps) {
|
|
|
262
467
|
usesLLM: eg.usesLLM,
|
|
263
468
|
fileCount: eg.fileCount,
|
|
264
469
|
});
|
|
470
|
+
// Also emit as a progress update so the UI shows real-time activity
|
|
471
|
+
sendEvent(deps, sessionId, {
|
|
472
|
+
type: "auto_research_progress",
|
|
473
|
+
projectId,
|
|
474
|
+
phase: "extension_generating",
|
|
475
|
+
message: `Generated: ${eg.name}`,
|
|
476
|
+
});
|
|
265
477
|
}
|
|
266
478
|
else if (t === "done") {
|
|
267
479
|
const d = parsed;
|
|
@@ -281,77 +493,38 @@ function processArLine(parsed, extensions, projectId, sessionId, deps) {
|
|
|
281
493
|
// Unknown event types are silently ignored for forward compatibility
|
|
282
494
|
}
|
|
283
495
|
// ---------------------------------------------------------------------------
|
|
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
496
|
// Task builder
|
|
300
497
|
// ---------------------------------------------------------------------------
|
|
301
498
|
/**
|
|
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
|
|
499
|
+
* Build the user task prompt sent as positional argument to pi.
|
|
500
|
+
* The system prompt (from the agent definition) provides the detailed
|
|
501
|
+
* instructions; this is just the project-specific context.
|
|
309
502
|
*/
|
|
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
|
-
});
|
|
503
|
+
function buildUserTask(projectPath, projectName) {
|
|
504
|
+
return [
|
|
505
|
+
`Analyze the project at "${projectPath}" named "${projectName}" to determine`,
|
|
506
|
+
`what custom pi coding agent extensions would accelerate development.`,
|
|
507
|
+
``,
|
|
508
|
+
`Follow your system prompt instructions for the full process:`,
|
|
509
|
+
`1. Context collection — scan the project structure`,
|
|
510
|
+
`2. Analysis — identify patterns and automation opportunities`,
|
|
511
|
+
`3. Extension generation — create .ts extension files`,
|
|
512
|
+
`4. Validation — verify the extensions are correct`,
|
|
513
|
+
``,
|
|
514
|
+
`Important: Read the template library at .pi/agents/auto-research-templates.md`,
|
|
515
|
+
`if it exists, for ready-to-adapt extension templates.`,
|
|
516
|
+
].join("\n");
|
|
517
|
+
}
|
|
518
|
+
// ---------------------------------------------------------------------------
|
|
519
|
+
// Temp file cleanup
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
function cleanupTemp(tmpDir) {
|
|
522
|
+
if (!tmpDir)
|
|
523
|
+
return;
|
|
524
|
+
try {
|
|
525
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
// best-effort cleanup
|
|
529
|
+
}
|
|
357
530
|
}
|