@mediadatafusion/pi-workflow-suite 0.0.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/CHANGELOG.md +13 -0
- package/CONTRIBUTING.md +9 -0
- package/LICENSE.md +201 -0
- package/NOTICE +6 -0
- package/README.md +1208 -0
- package/SECURITY.md +7 -0
- package/SUPPORT.md +9 -0
- package/TRADEMARKS.md +14 -0
- package/VERSION +1 -0
- package/agents/codebase-research.md +42 -0
- package/agents/general-worker.md +26 -0
- package/agents/implementation-planning.md +46 -0
- package/agents/quality-validation.md +43 -0
- package/agents/workflow-orchestrator.md +44 -0
- package/config/prompts/execute-approved-plan.md +43 -0
- package/config/prompts/mission-checkpoint.md +26 -0
- package/config/prompts/mission-final-validation.md +21 -0
- package/config/prompts/mission-plan.md +129 -0
- package/config/prompts/mission-repair.md +33 -0
- package/config/prompts/mission-run.md +37 -0
- package/config/prompts/validate-approved-plan.md +42 -0
- package/config/prompts/workflow-plan-prompt.md +93 -0
- package/config/prompts/workflow-repair.md +20 -0
- package/config/prompts/workflow-summary.md +23 -0
- package/config/workflow-settings.example.json +335 -0
- package/docs/assets/mediadatafusion-logo.png +0 -0
- package/docs/assets/pi-workflow-suite-card.png +0 -0
- package/docs/assets/pi-workflow-suite-header.png +0 -0
- package/docs/assets/pi-workflow-suite-video-thumb.png +0 -0
- package/docs/assets/readme-link-commands.svg +10 -0
- package/docs/assets/readme-link-install.svg +10 -0
- package/docs/assets/readme-link-quick-start.svg +10 -0
- package/docs/assets/readme-link-settings.svg +10 -0
- package/extensions/subagent/agents.ts +149 -0
- package/extensions/subagent/index.ts +1136 -0
- package/extensions/subagent/runner.ts +291 -0
- package/extensions/workflow-model-router.ts +1485 -0
- package/extensions/workflow-modes.ts +14778 -0
- package/extensions/workflow-parsers.ts +212 -0
- package/extensions/workflow-settings-capabilities.ts +282 -0
- package/extensions/workflow-state.ts +978 -0
- package/extensions/workflow-subagent-policy.ts +180 -0
- package/extensions/workflow-summary.ts +381 -0
- package/extensions/workflow-tool-guard.ts +302 -0
- package/extensions/workflow-validation-classifier.ts +102 -0
- package/extensions/workflow-web-tools.ts +356 -0
- package/package.json +1 -0
- package/scripts/audit-live.sh +69 -0
- package/scripts/audit-settings.sh +136 -0
- package/scripts/backup-live.sh +63 -0
- package/scripts/bootstrap-project.sh +220 -0
- package/scripts/install-to-live.sh +87 -0
- package/scripts/quarantine-live-junk.sh +69 -0
- package/scripts/verify-live.sh +128 -0
- package/skills/codebase-discovery/SKILL.md +20 -0
- package/skills/find-skills/SKILL.md +155 -0
- package/skills/git-safe-summary/SKILL.md +20 -0
- package/skills/implementation-planning/SKILL.md +20 -0
- package/skills/project-rules-audit/SKILL.md +20 -0
- package/skills/safe-execution/SKILL.md +20 -0
- package/skills/validation-review/SKILL.md +20 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Workflow Suite sub-agent runner.
|
|
3
|
+
*
|
|
4
|
+
* Used by the subagent tool and by workflow modes that must satisfy forced
|
|
5
|
+
* sub-agent policy before the main planner/executor/reviewer/validator turn.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
13
|
+
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import { type AgentConfig, type AgentScope, type AgentSource, discoverAgents } from "./agents.js";
|
|
15
|
+
|
|
16
|
+
export interface WorkflowSubagentTask {
|
|
17
|
+
agent: string;
|
|
18
|
+
task: string;
|
|
19
|
+
cwd?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkflowSubagentUsage {
|
|
23
|
+
input: number;
|
|
24
|
+
output: number;
|
|
25
|
+
cacheRead: number;
|
|
26
|
+
cacheWrite: number;
|
|
27
|
+
cost: number;
|
|
28
|
+
contextTokens: number;
|
|
29
|
+
turns: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface WorkflowSubagentResult {
|
|
33
|
+
agent: string;
|
|
34
|
+
agentSource: AgentSource | "unknown";
|
|
35
|
+
agentTools?: string[];
|
|
36
|
+
task: string;
|
|
37
|
+
exitCode: number;
|
|
38
|
+
output: string;
|
|
39
|
+
stderr: string;
|
|
40
|
+
usage: WorkflowSubagentUsage;
|
|
41
|
+
model?: string;
|
|
42
|
+
stopReason?: string;
|
|
43
|
+
errorMessage?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface WorkflowSubagentRunResult {
|
|
47
|
+
agentScope: AgentScope;
|
|
48
|
+
projectAgentsDir: string | null;
|
|
49
|
+
results: WorkflowSubagentResult[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface WorkflowSubagentRunOptions {
|
|
53
|
+
cwd: string;
|
|
54
|
+
tasks: WorkflowSubagentTask[];
|
|
55
|
+
agentScope?: AgentScope;
|
|
56
|
+
timeoutMinutes?: number;
|
|
57
|
+
staleMinutes?: number;
|
|
58
|
+
signal?: AbortSignal;
|
|
59
|
+
onUpdate?: (results: WorkflowSubagentResult[]) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const MAX_CONCURRENCY = 4;
|
|
63
|
+
|
|
64
|
+
function finalOutput(messages: Message[]): string {
|
|
65
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
66
|
+
const msg = messages[i];
|
|
67
|
+
if (msg.role !== "assistant") continue;
|
|
68
|
+
for (const part of msg.content) {
|
|
69
|
+
if (part.type === "text") return part.text;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function mapWithConcurrencyLimit<TIn, TOut>(items: TIn[], concurrency: number, fn: (item: TIn, index: number) => Promise<TOut>): Promise<TOut[]> {
|
|
76
|
+
if (items.length === 0) return [];
|
|
77
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
78
|
+
const results: TOut[] = new Array(items.length);
|
|
79
|
+
let nextIndex = 0;
|
|
80
|
+
const workers = new Array(limit).fill(null).map(async () => {
|
|
81
|
+
while (true) {
|
|
82
|
+
const current = nextIndex++;
|
|
83
|
+
if (current >= items.length) return;
|
|
84
|
+
results[current] = await fn(items[current], current);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
await Promise.all(workers);
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function writePromptToTempFile(agentName: string, prompt: string): Promise<{ dir: string; filePath: string }> {
|
|
92
|
+
const tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-subagent-"));
|
|
93
|
+
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
94
|
+
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
95
|
+
await withFileMutationQueue(filePath, async () => {
|
|
96
|
+
await fs.promises.writeFile(filePath, prompt, { encoding: "utf-8", mode: 0o600 });
|
|
97
|
+
});
|
|
98
|
+
return { dir: tmpDir, filePath };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
102
|
+
const currentScript = process.argv[1];
|
|
103
|
+
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
|
|
104
|
+
if (currentScript && !isBunVirtualScript && fs.existsSync(currentScript)) {
|
|
105
|
+
return { command: process.execPath, args: [currentScript, ...args] };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
109
|
+
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
110
|
+
if (!isGenericRuntime) return { command: process.execPath, args };
|
|
111
|
+
return { command: "pi", args };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function runSingleWorkflowSubagent(
|
|
115
|
+
defaultCwd: string,
|
|
116
|
+
agents: AgentConfig[],
|
|
117
|
+
task: WorkflowSubagentTask,
|
|
118
|
+
signal: AbortSignal | undefined,
|
|
119
|
+
limits: { timeoutMinutes?: number; staleMinutes?: number },
|
|
120
|
+
): Promise<WorkflowSubagentResult> {
|
|
121
|
+
const agent = agents.find((a) => a.name === task.agent);
|
|
122
|
+
if (!agent) {
|
|
123
|
+
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
124
|
+
return {
|
|
125
|
+
agent: task.agent,
|
|
126
|
+
agentSource: "unknown",
|
|
127
|
+
task: task.task,
|
|
128
|
+
exitCode: 1,
|
|
129
|
+
output: "",
|
|
130
|
+
stderr: `Unknown agent: "${task.agent}". Available agents: ${available}.`,
|
|
131
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const args: string[] = ["--no-extensions", "--mode", "json", "-p", "--no-session"];
|
|
136
|
+
if (agent.model) args.push("--model", agent.model);
|
|
137
|
+
if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
|
|
138
|
+
|
|
139
|
+
let tmpPromptDir: string | null = null;
|
|
140
|
+
let tmpPromptPath: string | null = null;
|
|
141
|
+
const messages: Message[] = [];
|
|
142
|
+
const usage: WorkflowSubagentUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 };
|
|
143
|
+
let stderr = "";
|
|
144
|
+
let model = agent.model;
|
|
145
|
+
let stopReason: string | undefined;
|
|
146
|
+
let errorMessage: string | undefined;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
if (agent.systemPrompt.trim()) {
|
|
150
|
+
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
151
|
+
tmpPromptDir = tmp.dir;
|
|
152
|
+
tmpPromptPath = tmp.filePath;
|
|
153
|
+
args.push("--append-system-prompt", tmpPromptPath);
|
|
154
|
+
}
|
|
155
|
+
args.push(`Task: ${task.task}`);
|
|
156
|
+
|
|
157
|
+
let wasAborted = false;
|
|
158
|
+
let timeoutReason = "";
|
|
159
|
+
const timeoutMs = Math.max(1, Math.min(240, Number(limits.timeoutMinutes ?? 20))) * 60_000;
|
|
160
|
+
const staleMs = Math.max(1, Math.min(240, Number(limits.staleMinutes ?? 8))) * 60_000;
|
|
161
|
+
|
|
162
|
+
const exitCode = await new Promise<number>((resolve) => {
|
|
163
|
+
const invocation = getPiInvocation(args);
|
|
164
|
+
const proc = spawn(invocation.command, invocation.args, {
|
|
165
|
+
cwd: task.cwd ?? defaultCwd,
|
|
166
|
+
shell: false,
|
|
167
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
168
|
+
env: {
|
|
169
|
+
...process.env,
|
|
170
|
+
PI_SUBAGENT_WORKER: "1",
|
|
171
|
+
PI_SUBAGENT_NAME: agent.name,
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
let buffer = "";
|
|
175
|
+
let lastOutputAt = Date.now();
|
|
176
|
+
let settled = false;
|
|
177
|
+
|
|
178
|
+
const stopProcess = (reason: string) => {
|
|
179
|
+
if (settled) return;
|
|
180
|
+
timeoutReason = reason;
|
|
181
|
+
wasAborted = true;
|
|
182
|
+
errorMessage = reason;
|
|
183
|
+
proc.kill("SIGTERM");
|
|
184
|
+
setTimeout(() => { if (!proc.killed) proc.kill("SIGKILL"); }, 5000);
|
|
185
|
+
};
|
|
186
|
+
const timeoutTimer = setTimeout(() => stopProcess(`Sub-agent timed out after ${Math.round(timeoutMs / 60000)} minute(s).`), timeoutMs);
|
|
187
|
+
const staleTimer = setInterval(() => {
|
|
188
|
+
if (Date.now() - lastOutputAt >= staleMs) stopProcess(`Sub-agent stale watchdog stopped worker after ${Math.round(staleMs / 60000)} minute(s) without parsed progress.`);
|
|
189
|
+
}, Math.min(staleMs, 60_000));
|
|
190
|
+
|
|
191
|
+
const processLine = (line: string) => {
|
|
192
|
+
if (!line.trim()) return;
|
|
193
|
+
let event: any;
|
|
194
|
+
try { event = JSON.parse(line); } catch { return; }
|
|
195
|
+
if (event.type === "message_end" && event.message) {
|
|
196
|
+
lastOutputAt = Date.now();
|
|
197
|
+
const msg = event.message as Message;
|
|
198
|
+
messages.push(msg);
|
|
199
|
+
if (msg.role === "assistant") {
|
|
200
|
+
usage.turns++;
|
|
201
|
+
const msgUsage = msg.usage;
|
|
202
|
+
if (msgUsage) {
|
|
203
|
+
usage.input += msgUsage.input || 0;
|
|
204
|
+
usage.output += msgUsage.output || 0;
|
|
205
|
+
usage.cacheRead += msgUsage.cacheRead || 0;
|
|
206
|
+
usage.cacheWrite += msgUsage.cacheWrite || 0;
|
|
207
|
+
usage.cost += msgUsage.cost?.total || 0;
|
|
208
|
+
usage.contextTokens = msgUsage.totalTokens || 0;
|
|
209
|
+
}
|
|
210
|
+
if (!model && msg.model) model = msg.model;
|
|
211
|
+
if (msg.stopReason) stopReason = msg.stopReason;
|
|
212
|
+
if (msg.errorMessage) errorMessage = msg.errorMessage;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (event.type === "tool_result_end" && event.message) {
|
|
216
|
+
lastOutputAt = Date.now();
|
|
217
|
+
messages.push(event.message as Message);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
proc.stdout.on("data", (data) => {
|
|
222
|
+
buffer += data.toString();
|
|
223
|
+
const lines = buffer.split("\n");
|
|
224
|
+
buffer = lines.pop() || "";
|
|
225
|
+
for (const line of lines) processLine(line);
|
|
226
|
+
});
|
|
227
|
+
proc.stderr.on("data", (data) => { stderr += data.toString(); });
|
|
228
|
+
proc.on("close", (code) => {
|
|
229
|
+
settled = true;
|
|
230
|
+
clearTimeout(timeoutTimer);
|
|
231
|
+
clearInterval(staleTimer);
|
|
232
|
+
if (buffer.trim()) processLine(buffer);
|
|
233
|
+
resolve(code ?? 0);
|
|
234
|
+
});
|
|
235
|
+
proc.on("error", () => {
|
|
236
|
+
settled = true;
|
|
237
|
+
clearTimeout(timeoutTimer);
|
|
238
|
+
clearInterval(staleTimer);
|
|
239
|
+
resolve(1);
|
|
240
|
+
});
|
|
241
|
+
if (signal) {
|
|
242
|
+
const killProc = () => stopProcess("Subagent was aborted");
|
|
243
|
+
if (signal.aborted) killProc();
|
|
244
|
+
else signal.addEventListener("abort", killProc, { once: true });
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
agent: agent.name,
|
|
250
|
+
agentSource: agent.source,
|
|
251
|
+
agentTools: agent.tools,
|
|
252
|
+
task: task.task,
|
|
253
|
+
exitCode: wasAborted ? 1 : exitCode,
|
|
254
|
+
output: finalOutput(messages),
|
|
255
|
+
stderr,
|
|
256
|
+
usage,
|
|
257
|
+
model,
|
|
258
|
+
stopReason: wasAborted ? "aborted" : stopReason,
|
|
259
|
+
errorMessage: wasAborted ? (timeoutReason || "Subagent was aborted") : errorMessage,
|
|
260
|
+
};
|
|
261
|
+
} finally {
|
|
262
|
+
if (tmpPromptPath) try { fs.unlinkSync(tmpPromptPath); } catch { /* ignore */ }
|
|
263
|
+
if (tmpPromptDir) try { fs.rmdirSync(tmpPromptDir); } catch { /* ignore */ }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export async function runWorkflowSubagents(options: WorkflowSubagentRunOptions): Promise<WorkflowSubagentRunResult> {
|
|
268
|
+
const agentScope = options.agentScope ?? "user";
|
|
269
|
+
const discovery = discoverAgents(options.cwd, agentScope);
|
|
270
|
+
const running: WorkflowSubagentResult[] = options.tasks.map((task) => ({
|
|
271
|
+
agent: task.agent,
|
|
272
|
+
agentSource: "unknown",
|
|
273
|
+
task: task.task,
|
|
274
|
+
exitCode: -1,
|
|
275
|
+
output: "",
|
|
276
|
+
stderr: "",
|
|
277
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
278
|
+
}));
|
|
279
|
+
options.onUpdate?.([...running]);
|
|
280
|
+
const results = await mapWithConcurrencyLimit(options.tasks, MAX_CONCURRENCY, async (task, index) => {
|
|
281
|
+
const result = await runSingleWorkflowSubagent(options.cwd, discovery.agents, task, options.signal, { timeoutMinutes: options.timeoutMinutes, staleMinutes: options.staleMinutes });
|
|
282
|
+
running[index] = result;
|
|
283
|
+
options.onUpdate?.([...running]);
|
|
284
|
+
return result;
|
|
285
|
+
});
|
|
286
|
+
return { agentScope, projectAgentsDir: discovery.projectAgentsDir, results };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function workflowSubagentResultOutput(result: WorkflowSubagentResult): string {
|
|
290
|
+
return result.output || result.errorMessage || result.stderr || "(no output)";
|
|
291
|
+
}
|