@mediadatafusion/pi-workflow-suite 0.0.10 → 0.0.13
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/CHANGELOG.md +73 -0
- package/README.md +146 -20
- package/VERSION +1 -1
- package/agents/codebase-research.md +7 -5
- package/agents/general-worker.md +9 -7
- package/agents/implementation-planning.md +5 -3
- package/agents/quality-validation.md +9 -8
- package/agents/workflow-orchestrator.md +9 -7
- package/config/prompts/execute-approved-plan.md +12 -2
- package/config/prompts/mission-final-validation.md +38 -5
- package/config/prompts/mission-plan.md +17 -1
- package/config/prompts/mission-repair.md +16 -2
- package/config/prompts/mission-review-prompt.md +55 -0
- package/config/prompts/mission-run.md +18 -5
- package/config/prompts/validate-approved-plan.md +57 -3
- package/config/prompts/workflow-plan-prompt.md +11 -1
- package/config/prompts/workflow-repair.md +18 -2
- package/config/prompts/workflow-reviewer-prompt.md +60 -0
- package/config/prompts/workflow-summary.md +1 -4
- package/config/workflow-settings.example.json +13 -11
- package/extensions/subagent/index.ts +41 -18
- package/extensions/subagent/repolock-guard.ts +224 -4
- package/extensions/subagent/runner.ts +136 -12
- package/extensions/workflow-model-router.ts +152 -55
- package/extensions/workflow-modes.ts +4784 -1087
- package/extensions/workflow-settings-capabilities.ts +10 -0
- package/extensions/workflow-state.ts +139 -15
- package/extensions/workflow-subagent-policy.ts +13 -1
- package/extensions/workflow-summary.ts +8 -19
- package/extensions/workflow-tool-guard.ts +420 -39
- package/extensions/workflow-validation-classifier.ts +46 -4
- package/extensions/workflow-web-tools.ts +361 -1
- package/package.json +9 -5
- package/scripts/audit-live.sh +1 -1
- package/scripts/build-package-export.mjs +8 -13
- package/scripts/check-clean-release-tree.sh +3 -2
- package/scripts/check-package-media.mjs +78 -0
- package/scripts/install-to-live.sh +2 -0
- package/scripts/package-media-config.mjs +28 -0
- package/scripts/prepare-package-readme.mjs +19 -18
- package/scripts/quarantine-live-junk.sh +1 -1
- package/scripts/verify-live.sh +9 -1
- package/skills/implementation-planning/SKILL.md +1 -1
- package/skills/safe-execution/SKILL.md +1 -1
- package/skills/validation-review/SKILL.md +1 -1
|
@@ -9,6 +9,7 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
9
9
|
import * as fs from "node:fs";
|
|
10
10
|
import * as os from "node:os";
|
|
11
11
|
import * as path from "node:path";
|
|
12
|
+
import * as crypto from "node:crypto";
|
|
12
13
|
import type { Message } from "@earendil-works/pi-ai";
|
|
13
14
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
import { loadWorkflowSettings } from "../workflow-model-router.js";
|
|
@@ -18,6 +19,12 @@ export interface WorkflowSubagentTask {
|
|
|
18
19
|
agent: string;
|
|
19
20
|
task: string;
|
|
20
21
|
cwd?: string;
|
|
22
|
+
schema?: Record<string, unknown>;
|
|
23
|
+
background?: boolean;
|
|
24
|
+
model?: string;
|
|
25
|
+
skills?: string;
|
|
26
|
+
output?: string;
|
|
27
|
+
workflowPhase?: string;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export interface WorkflowSubagentUsage {
|
|
@@ -42,6 +49,7 @@ export interface WorkflowSubagentResult {
|
|
|
42
49
|
model?: string;
|
|
43
50
|
stopReason?: string;
|
|
44
51
|
errorMessage?: string;
|
|
52
|
+
parsedOutput?: unknown;
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
export interface WorkflowSubagentRunResult {
|
|
@@ -58,9 +66,50 @@ export interface WorkflowSubagentRunOptions {
|
|
|
58
66
|
staleMinutes?: number;
|
|
59
67
|
signal?: AbortSignal;
|
|
60
68
|
onUpdate?: (results: WorkflowSubagentResult[]) => void;
|
|
69
|
+
concurrency?: number;
|
|
70
|
+
failFast?: boolean;
|
|
71
|
+
background?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
75
|
+
|
|
76
|
+
// ── Orphan process tracking (#8) ──────────────────────────────
|
|
77
|
+
const trackedPids = new Set<number>();
|
|
78
|
+
|
|
79
|
+
export function trackedOrphanPids(): ReadonlySet<number> {
|
|
80
|
+
return trackedPids;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function trackSubagentPid(pid: number): void {
|
|
84
|
+
trackedPids.add(pid);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function untrackSubagentPid(pid: number): void {
|
|
88
|
+
trackedPids.delete(pid);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function cleanupOrphanProcesses(): void {
|
|
92
|
+
for (const pid of trackedPids) {
|
|
93
|
+
try { process.kill(pid, "SIGTERM"); } catch { /* already dead */ }
|
|
94
|
+
}
|
|
95
|
+
trackedPids.clear();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Clean up on parent exit (unexpected death)
|
|
99
|
+
if (typeof process.on === "function") {
|
|
100
|
+
process.on("exit", () => { for (const pid of trackedPids) { try { process.kill(pid, "SIGTERM"); } catch { /* ignore */ } } });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Result caching (#6) ─────────────────────────────────────
|
|
104
|
+
const resultCache = new Map<string, WorkflowSubagentResult>();
|
|
105
|
+
|
|
106
|
+
function cacheKey(agent: string, task: string, cwd: string): string {
|
|
107
|
+
return crypto.createHash("sha256").update(`${agent}\n${task}\n${cwd}`).digest("hex");
|
|
61
108
|
}
|
|
62
109
|
|
|
63
|
-
|
|
110
|
+
export function clearSubagentResultCache(): void {
|
|
111
|
+
resultCache.clear();
|
|
112
|
+
}
|
|
64
113
|
const REPOLOCK_GUARD_EXTENSION = path.join(path.dirname(new URL(import.meta.url).pathname), "repolock-guard.ts");
|
|
65
114
|
|
|
66
115
|
function safeRealpath(candidate: string): string {
|
|
@@ -104,19 +153,30 @@ function finalOutput(messages: Message[]): string {
|
|
|
104
153
|
return "";
|
|
105
154
|
}
|
|
106
155
|
|
|
107
|
-
async function mapWithConcurrencyLimit<TIn, TOut>(items: TIn[], concurrency: number, fn: (item: TIn, index: number) => Promise<TOut
|
|
156
|
+
async function mapWithConcurrencyLimit<TIn, TOut>(items: TIn[], concurrency: number, fn: (item: TIn, index: number) => Promise<TOut>, failFast = false): Promise<TOut[]> {
|
|
108
157
|
if (items.length === 0) return [];
|
|
109
158
|
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
110
159
|
const results: TOut[] = new Array(items.length);
|
|
111
160
|
let nextIndex = 0;
|
|
161
|
+
let firstError: Error | undefined;
|
|
112
162
|
const workers = new Array(limit).fill(null).map(async () => {
|
|
113
163
|
while (true) {
|
|
164
|
+
if (failFast && firstError) return;
|
|
114
165
|
const current = nextIndex++;
|
|
115
166
|
if (current >= items.length) return;
|
|
116
|
-
|
|
167
|
+
try {
|
|
168
|
+
results[current] = await fn(items[current], current);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (failFast) {
|
|
171
|
+
firstError = err instanceof Error ? err : new Error(String(err));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
117
176
|
}
|
|
118
177
|
});
|
|
119
178
|
await Promise.all(workers);
|
|
179
|
+
if (failFast && firstError) throw firstError;
|
|
120
180
|
return results;
|
|
121
181
|
}
|
|
122
182
|
|
|
@@ -166,6 +226,12 @@ async function runSingleWorkflowSubagent(
|
|
|
166
226
|
|
|
167
227
|
const lockRoot = repoLockRootForSubagent(defaultCwd);
|
|
168
228
|
const effectiveCwd = resolveSubagentCwd(task.cwd, defaultCwd);
|
|
229
|
+
|
|
230
|
+
// ── Result caching (#6): check cache before spawning ──
|
|
231
|
+
const key = cacheKey(agent.name, task.task, effectiveCwd);
|
|
232
|
+
const cached = signal?.aborted ? undefined : resultCache.get(key);
|
|
233
|
+
if (cached) return { ...cached, output: `${cached.output}\n\n[cached]` };
|
|
234
|
+
|
|
169
235
|
if (lockRoot && !pathInsideRoot(effectiveCwd, lockRoot)) {
|
|
170
236
|
return {
|
|
171
237
|
agent: task.agent,
|
|
@@ -188,7 +254,7 @@ async function runSingleWorkflowSubagent(
|
|
|
188
254
|
const messages: Message[] = [];
|
|
189
255
|
const usage: WorkflowSubagentUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
190
256
|
let stderr = "";
|
|
191
|
-
let model = agent.model;
|
|
257
|
+
let model = task.model || agent.model;
|
|
192
258
|
let stopReason: string | undefined;
|
|
193
259
|
let errorMessage: string | undefined;
|
|
194
260
|
|
|
@@ -199,7 +265,11 @@ async function runSingleWorkflowSubagent(
|
|
|
199
265
|
tmpPromptPath = tmp.filePath;
|
|
200
266
|
args.push("--append-system-prompt", tmpPromptPath);
|
|
201
267
|
}
|
|
202
|
-
|
|
268
|
+
// ── Structured output (#5): inject schema if present ──
|
|
269
|
+
const schemaInstruction = task.schema
|
|
270
|
+
? `\n\nReturn your final result as a single valid JSON object matching this schema:\n${JSON.stringify(task.schema, null, 2)}\n\nWrap ONLY the JSON object in a \`\`\`json code block at the end of your response.`
|
|
271
|
+
: "";
|
|
272
|
+
args.push(`Task: ${task.task}${schemaInstruction}`);
|
|
203
273
|
|
|
204
274
|
let wasAborted = false;
|
|
205
275
|
let timeoutReason = "";
|
|
@@ -216,9 +286,13 @@ async function runSingleWorkflowSubagent(
|
|
|
216
286
|
...process.env,
|
|
217
287
|
PI_SUBAGENT_WORKER: "1",
|
|
218
288
|
PI_SUBAGENT_NAME: agent.name,
|
|
289
|
+
...(task.workflowPhase ? { PI_WORKFLOW_SUBAGENT_PHASE: task.workflowPhase } : {}),
|
|
219
290
|
...(lockRoot ? { PI_WORKFLOW_REPO_LOCK_ENABLED: "1", PI_WORKFLOW_REPO_LOCK_ROOT: lockRoot } : {}),
|
|
291
|
+
...(task.skills ? { PI_SUBAGENT_SKILLS: task.skills } : {}),
|
|
292
|
+
...(task.output ? { PI_SUBAGENT_OUTPUT: task.output } : {}),
|
|
220
293
|
},
|
|
221
294
|
});
|
|
295
|
+
trackedPids.add(proc.pid!);
|
|
222
296
|
let buffer = "";
|
|
223
297
|
let lastOutputAt = Date.now();
|
|
224
298
|
let settled = false;
|
|
@@ -228,8 +302,8 @@ async function runSingleWorkflowSubagent(
|
|
|
228
302
|
timeoutReason = reason;
|
|
229
303
|
wasAborted = true;
|
|
230
304
|
errorMessage = reason;
|
|
231
|
-
proc.kill("SIGTERM");
|
|
232
|
-
setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 5000);
|
|
305
|
+
try { process.kill(-proc.pid!, "SIGTERM"); } catch { proc.kill("SIGTERM"); }
|
|
306
|
+
setTimeout(() => { if (!proc.killed) { try { process.kill(-proc.pid!, "SIGKILL"); } catch { proc.kill("SIGKILL"); } } }, 5000);
|
|
233
307
|
};
|
|
234
308
|
const timeoutTimer = setTimeout(() => stopProcess(`Sub-agent timed out after ${Math.round(timeoutMs / 60000)} minute(s).`), timeoutMs);
|
|
235
309
|
const staleTimer = setInterval(() => {
|
|
@@ -275,13 +349,19 @@ async function runSingleWorkflowSubagent(
|
|
|
275
349
|
proc.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
276
350
|
proc.on("close", (code) => {
|
|
277
351
|
settled = true;
|
|
352
|
+
trackedPids.delete(proc.pid!);
|
|
278
353
|
clearTimeout(timeoutTimer);
|
|
279
354
|
clearInterval(staleTimer);
|
|
280
355
|
if (buffer.trim()) processLine(buffer);
|
|
356
|
+
// Kill process group to clean up background child processes
|
|
357
|
+
// (dev servers, static servers, tools — any program the sub-agent started).
|
|
358
|
+
// process.kill(-pid) signals the entire process group; works on all Unix.
|
|
359
|
+
try { if (proc.pid) process.kill(-proc.pid, "SIGTERM"); } catch { /* group empty */ }
|
|
281
360
|
resolve(code ?? 0);
|
|
282
361
|
});
|
|
283
362
|
proc.on("error", () => {
|
|
284
363
|
settled = true;
|
|
364
|
+
trackedPids.delete(proc.pid!);
|
|
285
365
|
clearTimeout(timeoutTimer);
|
|
286
366
|
clearInterval(staleTimer);
|
|
287
367
|
resolve(1);
|
|
@@ -293,19 +373,41 @@ async function runSingleWorkflowSubagent(
|
|
|
293
373
|
}
|
|
294
374
|
});
|
|
295
375
|
|
|
296
|
-
|
|
376
|
+
const rawOutput = finalOutput(messages);
|
|
377
|
+
// ── Structured output (#5): try JSON parse against schema ──
|
|
378
|
+
let parsedOutput: unknown;
|
|
379
|
+
if (task.schema && rawOutput) {
|
|
380
|
+
const jsonMatch = rawOutput.match(/```json\s*([\s\S]*?)\s*```/);
|
|
381
|
+
const candidate = jsonMatch ? jsonMatch[1].trim() : rawOutput.trim();
|
|
382
|
+
try { parsedOutput = JSON.parse(candidate); } catch { /* free-form output, not JSON */ }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const result: WorkflowSubagentResult = {
|
|
297
386
|
agent: agent.name,
|
|
298
387
|
agentSource: agent.source,
|
|
299
388
|
agentTools: agent.tools,
|
|
300
389
|
task: task.task,
|
|
301
390
|
exitCode: wasAborted ? 1 : exitCode,
|
|
302
|
-
output:
|
|
391
|
+
output: rawOutput,
|
|
303
392
|
stderr,
|
|
304
393
|
usage,
|
|
305
394
|
model,
|
|
306
395
|
stopReason: wasAborted ? "aborted" : stopReason,
|
|
307
396
|
errorMessage: wasAborted ? (timeoutReason || "Subagent was aborted") : errorMessage,
|
|
397
|
+
parsedOutput,
|
|
308
398
|
};
|
|
399
|
+
|
|
400
|
+
// ── Result caching (#6): store successful results ──
|
|
401
|
+
if (!wasAborted && exitCode === 0 && !signal?.aborted) {
|
|
402
|
+
resultCache.set(key, result);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Retry-on-timeout (#3): retry once on timeout/stale ──
|
|
406
|
+
if (wasAborted && timeoutReason && !signal?.aborted) {
|
|
407
|
+
return result; // single attempt; retry is handled at the runWorkflowSubagents level
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return result;
|
|
309
411
|
} finally {
|
|
310
412
|
if (tmpPromptPath) try { fs.unlinkSync(tmpPromptPath); } catch { /* ignore */ }
|
|
311
413
|
if (tmpPromptDir) try { fs.rmdirSync(tmpPromptDir); } catch { /* ignore */ }
|
|
@@ -325,12 +427,34 @@ export async function runWorkflowSubagents(options: WorkflowSubagentRunOptions):
|
|
|
325
427
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
326
428
|
}));
|
|
327
429
|
options.onUpdate?.([...running]);
|
|
328
|
-
|
|
329
|
-
|
|
430
|
+
|
|
431
|
+
const executeTask = async (task: WorkflowSubagentTask, index: number): Promise<WorkflowSubagentResult> => {
|
|
432
|
+
const limits = { timeoutMinutes: options.timeoutMinutes, staleMinutes: options.staleMinutes };
|
|
433
|
+
let result = await runSingleWorkflowSubagent(options.cwd, discovery.agents, task, options.signal, limits);
|
|
434
|
+
// ── Retry-on-timeout (#3): retry once on timeout/stale ──
|
|
435
|
+
if (result.exitCode !== 0 && result.stopReason === "aborted" && result.errorMessage?.includes("timed out") && !options.signal?.aborted) {
|
|
436
|
+
const retryResult = await runSingleWorkflowSubagent(options.cwd, discovery.agents, task, options.signal, limits);
|
|
437
|
+
retryResult.output = `[retry after timeout]\n${retryResult.output}`;
|
|
438
|
+
result = retryResult;
|
|
439
|
+
}
|
|
330
440
|
running[index] = result;
|
|
331
441
|
options.onUpdate?.([...running]);
|
|
332
442
|
return result;
|
|
333
|
-
}
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
|
|
446
|
+
|
|
447
|
+
if (options.background) {
|
|
448
|
+
// Fire-and-forget: start execution, don't await, deliver results via onUpdate
|
|
449
|
+
mapWithConcurrencyLimit(options.tasks, concurrency, executeTask, options.failFast).then((results) => {
|
|
450
|
+
// Results delivered via onUpdate during execution; final result available for next turn
|
|
451
|
+
}).catch(() => {
|
|
452
|
+
// Background failures are non-fatal; onUpdate already reported individual failures
|
|
453
|
+
});
|
|
454
|
+
return { agentScope, projectAgentsDir: discovery.projectAgentsDir, results: running };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const results = await mapWithConcurrencyLimit(options.tasks, concurrency, executeTask, options.failFast);
|
|
334
458
|
return { agentScope, projectAgentsDir: discovery.projectAgentsDir, results };
|
|
335
459
|
}
|
|
336
460
|
|