@matyah00/openpi 0.1.5 → 0.2.0
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 +29 -11
- package/agents/agent-chain.yaml +50 -0
- package/agents/api-designer.md +58 -0
- package/agents/docs-writer.md +38 -0
- package/agents/migration-expert.md +62 -0
- package/agents/perf-auditor.md +64 -0
- package/agents/teams.yaml +23 -0
- package/damage-control-rules.yaml +153 -0
- package/extensions/agent-chain.ts +101 -12
- package/extensions/agent-team.ts +11 -0
- package/extensions/audit-tools.ts +125 -6
- package/extensions/lib/auditLogger.ts +29 -0
- package/extensions/openpi.ts +169 -21
- package/extensions/search-tools.ts +21 -3
- package/extensions/workflow.ts +77 -5
- package/package.json +7 -3
- package/prompts/docs.md +37 -0
- package/prompts/migrate.md +44 -0
- package/prompts/perf.md +52 -0
- package/prompts/refactor.md +53 -0
- package/scripts/validate-package.mjs +28 -1
- package/skills/perf-auditor/SKILL.md +49 -0
- package/skills/refactor-guide/SKILL.md +39 -0
- package/tsconfig.json +2 -1
package/extensions/openpi.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
-
import { extensionDir } from "./lib/packagePaths.ts";
|
|
5
|
+
import { extensionDir, bundledPromptsDir, bundledAgentsDir, bundledPiPiAgentsDir } from "./lib/packagePaths.ts";
|
|
6
|
+
import { parseMarkdownFrontmatter, stringField, arrayField } from "./lib/markdown.ts";
|
|
6
7
|
|
|
7
8
|
type Profile = {
|
|
8
9
|
name: string;
|
|
@@ -130,26 +131,31 @@ function resolveSettingsPath(cwd: string, global: boolean): string {
|
|
|
130
131
|
return global ? path.join(os.homedir(), ".pi", "agent", "settings.json") : path.join(cwd, ".pi", "settings.json");
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
function applyProfileToSettings(settingsPath: string, profile: Profile): { added: string[]; removed: number } {
|
|
134
|
+
function applyProfileToSettings(settingsPath: string, profile: Profile, dryRun = false): { added: string[]; removed: number } {
|
|
134
135
|
const settingsDir = path.dirname(settingsPath);
|
|
135
136
|
const settings = readJsonFile(settingsPath);
|
|
136
137
|
const currentExtensions = stringArray(settings.extensions);
|
|
137
138
|
const keptExtensions = currentExtensions.filter((entry) => !isManagedExtensionEntry(entry, settingsDir));
|
|
138
139
|
const added = profile.extensions.map(extensionPath);
|
|
139
140
|
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
if (!dryRun) {
|
|
142
|
+
settings.extensions = addUnique(keptExtensions, added);
|
|
143
|
+
writeJsonFile(settingsPath, settings);
|
|
144
|
+
}
|
|
142
145
|
|
|
143
146
|
return { added, removed: currentExtensions.length - keptExtensions.length };
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
function clearProfileFromSettings(settingsPath: string): { removed: number } {
|
|
149
|
+
function clearProfileFromSettings(settingsPath: string, dryRun = false): { removed: number } {
|
|
147
150
|
const settingsDir = path.dirname(settingsPath);
|
|
148
151
|
const settings = readJsonFile(settingsPath);
|
|
149
152
|
const currentExtensions = stringArray(settings.extensions);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
const keptExtensions = currentExtensions.filter((entry) => !isManagedExtensionEntry(entry, settingsDir));
|
|
154
|
+
if (!dryRun) {
|
|
155
|
+
settings.extensions = keptExtensions;
|
|
156
|
+
writeJsonFile(settingsPath, settings);
|
|
157
|
+
}
|
|
158
|
+
return { removed: currentExtensions.length - keptExtensions.length };
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
function profileListMarkdown(): string {
|
|
@@ -157,6 +163,7 @@ function profileListMarkdown(): string {
|
|
|
157
163
|
"# openpi profiles",
|
|
158
164
|
"",
|
|
159
165
|
"Use `/openpi use <profile>` to activate a profile for this project.",
|
|
166
|
+
"Use `/openpi use <profile1>+<profile2>` to combine multiple profiles (e.g. `commands+guard`).",
|
|
160
167
|
"Use `/openpi use <profile> --global` to activate it globally.",
|
|
161
168
|
"Run `/reload` or restart Pi after changing profiles.",
|
|
162
169
|
"",
|
|
@@ -172,6 +179,110 @@ function emit(pi: ExtensionAPI, content: string) {
|
|
|
172
179
|
pi.sendMessage({ customType: "openpi", content, display: true });
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
const NATIVE_TOOLS_BY_EXTENSION: Record<string, string[]> = {
|
|
183
|
+
"search-tools.ts": ["project_tree", "code_search_batch"],
|
|
184
|
+
"audit-tools.ts": ["env_scan", "secret_scan", "ghost_test_scan", "dependency_inventory", "sast_scan"],
|
|
185
|
+
"state-tools.ts": ["state_snapshot", "state_restore", "state_diff", "state_clean"],
|
|
186
|
+
"agent-team.ts": ["dispatch_agent"],
|
|
187
|
+
"agent-chain.ts": ["run_chain"],
|
|
188
|
+
"workflow.ts": ["spawn_agents"],
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const NATIVE_COMMANDS_BY_EXTENSION: Record<string, string[]> = {
|
|
192
|
+
"workflow.ts": ["add", "fix", "review", "openpi-agents"],
|
|
193
|
+
"theme-cycler.ts": ["theme"],
|
|
194
|
+
"system-select.ts": ["system"],
|
|
195
|
+
"agent-team.ts": ["agents-team", "agents-list"],
|
|
196
|
+
"agent-chain.ts": ["chain", "chain-list"],
|
|
197
|
+
"commands.ts": ["commands", "command:status"],
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
function scanDirForFiles(dir: string, extension: string): string[] {
|
|
201
|
+
if (!fs.existsSync(dir)) return [];
|
|
202
|
+
try {
|
|
203
|
+
return fs.readdirSync(dir)
|
|
204
|
+
.filter((file) => file.endsWith(extension))
|
|
205
|
+
.map((file) => path.basename(file, extension).toLowerCase());
|
|
206
|
+
} catch {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getProfileSummary(extensions: string[], cwd: string) {
|
|
212
|
+
const tools = new Set<string>();
|
|
213
|
+
const commands = new Set<string>();
|
|
214
|
+
|
|
215
|
+
for (const ext of extensions) {
|
|
216
|
+
const extTools = NATIVE_TOOLS_BY_EXTENSION[ext] || [];
|
|
217
|
+
for (const t of extTools) {
|
|
218
|
+
tools.add(t);
|
|
219
|
+
}
|
|
220
|
+
const extCmds = NATIVE_COMMANDS_BY_EXTENSION[ext] || [];
|
|
221
|
+
for (const c of extCmds) {
|
|
222
|
+
commands.add(c);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// If commands.ts is active, also scan prompts
|
|
227
|
+
if (extensions.includes("commands.ts")) {
|
|
228
|
+
const promptDirs = [
|
|
229
|
+
bundledPromptsDir,
|
|
230
|
+
path.join(cwd, ".pi", "prompts")
|
|
231
|
+
];
|
|
232
|
+
for (const dir of promptDirs) {
|
|
233
|
+
if (fs.existsSync(dir)) {
|
|
234
|
+
try {
|
|
235
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
236
|
+
for (const file of files) {
|
|
237
|
+
const filePath = path.join(dir, file);
|
|
238
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
239
|
+
const { frontmatter } = parseMarkdownFrontmatter(raw);
|
|
240
|
+
const name = stringField(frontmatter.name) || path.basename(file, ".md");
|
|
241
|
+
commands.add(name.toLowerCase());
|
|
242
|
+
const aliases = arrayField(frontmatter.aliases);
|
|
243
|
+
for (const alias of aliases) {
|
|
244
|
+
commands.add(alias.toLowerCase());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch {
|
|
248
|
+
// ignore
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Scan agents
|
|
255
|
+
const agents = new Set<string>();
|
|
256
|
+
const agentDirs = [
|
|
257
|
+
bundledAgentsDir,
|
|
258
|
+
bundledPiPiAgentsDir,
|
|
259
|
+
path.join(cwd, ".pi", "agents"),
|
|
260
|
+
path.join(cwd, "agents")
|
|
261
|
+
];
|
|
262
|
+
for (const dir of agentDirs) {
|
|
263
|
+
if (fs.existsSync(dir)) {
|
|
264
|
+
try {
|
|
265
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
266
|
+
for (const file of files) {
|
|
267
|
+
const agentName = path.basename(file, ".md").toLowerCase();
|
|
268
|
+
agents.add(agentName);
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// ignore
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
commandsCount: commands.size,
|
|
278
|
+
commandsList: Array.from(commands),
|
|
279
|
+
toolsCount: tools.size,
|
|
280
|
+
toolsList: Array.from(tools),
|
|
281
|
+
agentsCount: agents.size,
|
|
282
|
+
agentsList: Array.from(agents),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
175
286
|
function registerProfileCommand(pi: ExtensionAPI, name: string, description: string) {
|
|
176
287
|
pi.registerCommand(name, {
|
|
177
288
|
description,
|
|
@@ -185,6 +296,16 @@ function registerProfileCommand(pi: ExtensionAPI, name: string, description: str
|
|
|
185
296
|
}
|
|
186
297
|
if (words[0] === "use") {
|
|
187
298
|
const partial = words[1] || "";
|
|
299
|
+
if (partial.includes("+")) {
|
|
300
|
+
const parts = partial.split("+");
|
|
301
|
+
const last = parts[parts.length - 1];
|
|
302
|
+
const base = parts.slice(0, -1).join("+");
|
|
303
|
+
return PROFILES.filter((profile) => profile.name.startsWith(last)).map((profile) => ({
|
|
304
|
+
value: `${base}+${profile.name}`,
|
|
305
|
+
label: `${base}+${profile.name}`,
|
|
306
|
+
description: `Combine with ${profile.name}`,
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
188
309
|
return PROFILES.filter((profile) => profile.name.startsWith(partial)).map((profile) => ({
|
|
189
310
|
value: profile.name,
|
|
190
311
|
label: profile.name,
|
|
@@ -202,36 +323,62 @@ function registerProfileCommand(pi: ExtensionAPI, name: string, description: str
|
|
|
202
323
|
return;
|
|
203
324
|
}
|
|
204
325
|
|
|
205
|
-
|
|
326
|
+
const isComposition = action.includes("+") && action.split("+").every(name => PROFILE_BY_NAME.has(name));
|
|
327
|
+
if (PROFILE_BY_NAME.has(action) || isComposition) tokens.unshift("use");
|
|
206
328
|
|
|
207
329
|
if (tokens[0] === "use") {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
330
|
+
const profileArg = tokens[1] || "";
|
|
331
|
+
const profileNames = profileArg.split("+").filter(Boolean);
|
|
332
|
+
if (!profileNames.length) {
|
|
333
|
+
ctx.ui.notify("Usage: /openpi use <profile1>+<profile2> [--global] [--dry-run]", "error");
|
|
211
334
|
return;
|
|
212
335
|
}
|
|
336
|
+
|
|
337
|
+
const invalidNames = profileNames.filter(name => !PROFILE_BY_NAME.has(name));
|
|
338
|
+
if (invalidNames.length > 0) {
|
|
339
|
+
ctx.ui.notify(`Unknown profile(s): ${invalidNames.join(", ")}`, "error");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const profiles = profileNames.map(name => PROFILE_BY_NAME.get(name)!);
|
|
344
|
+
const composedProfile: Profile = {
|
|
345
|
+
name: profileNames.join("+"),
|
|
346
|
+
description: profiles.map(p => p.description).join(" | "),
|
|
347
|
+
extensions: Array.from(new Set(profiles.flatMap(p => p.extensions))),
|
|
348
|
+
notes: Array.from(new Set(profiles.flatMap(p => p.notes || [])))
|
|
349
|
+
};
|
|
350
|
+
|
|
213
351
|
const global = tokens.includes("--global");
|
|
352
|
+
const dryRun = tokens.includes("--dry-run");
|
|
214
353
|
const settingsPath = resolveSettingsPath(ctx.cwd, global);
|
|
215
|
-
const result = applyProfileToSettings(settingsPath,
|
|
354
|
+
const result = applyProfileToSettings(settingsPath, composedProfile, dryRun);
|
|
355
|
+
|
|
356
|
+
const summary = getProfileSummary(composedProfile.extensions, ctx.cwd);
|
|
216
357
|
emit(pi, [
|
|
217
|
-
|
|
358
|
+
`${dryRun ? "**[DRY-RUN]** " : ""}Activated **${composedProfile.name}** in ${global ? "global" : "project"} Pi settings.`,
|
|
218
359
|
"",
|
|
219
360
|
`Settings: \`${settingsPath}\``,
|
|
220
|
-
|
|
221
|
-
"Added:
|
|
222
|
-
...result.added.map((entry) => `- \`${entry}\``),
|
|
361
|
+
`${dryRun ? "Would remove" : "Removed"} previous openpi profile entries: ${result.removed}`,
|
|
362
|
+
`${dryRun ? "Would add" : "Added"} extensions: ${result.added.length}`,
|
|
363
|
+
...result.added.map((entry) => `- \`${path.basename(entry)}\``),
|
|
223
364
|
"",
|
|
224
|
-
"
|
|
225
|
-
|
|
365
|
+
"### Profile Capability Summary",
|
|
366
|
+
`- **Commands Enabled**: ${summary.commandsCount} (${summary.commandsList.join(", ")})`,
|
|
367
|
+
`- **Tools Enabled**: ${summary.toolsCount} (${summary.toolsList.join(", ")})`,
|
|
368
|
+
`- **Agents Available**: ${summary.agentsCount} (${summary.agentsList.join(", ")})`,
|
|
369
|
+
"",
|
|
370
|
+
dryRun ? "This was a dry run. No settings files were modified." : "Run `/reload` or restart Pi to apply changes.",
|
|
371
|
+
...(composedProfile.notes?.length ? ["", ...composedProfile.notes.map((note) => `- ${note}`)] : []),
|
|
226
372
|
].join("\n"));
|
|
227
373
|
return;
|
|
228
374
|
}
|
|
229
375
|
|
|
230
376
|
if (tokens[0] === "clear") {
|
|
231
377
|
const global = tokens.includes("--global");
|
|
378
|
+
const dryRun = tokens.includes("--dry-run");
|
|
232
379
|
const settingsPath = resolveSettingsPath(ctx.cwd, global);
|
|
233
|
-
const result = clearProfileFromSettings(settingsPath);
|
|
234
|
-
emit(pi,
|
|
380
|
+
const result = clearProfileFromSettings(settingsPath, dryRun);
|
|
381
|
+
emit(pi, `${dryRun ? "**[DRY-RUN]** Would clear" : "Cleared"} ${result.removed} openpi extension entr${result.removed === 1 ? "y" : "ies"} from \`${settingsPath}\`.${dryRun ? "" : " Run `/reload` or restart Pi."}`);
|
|
235
382
|
return;
|
|
236
383
|
}
|
|
237
384
|
|
|
@@ -240,6 +387,7 @@ function registerProfileCommand(pi: ExtensionAPI, name: string, description: str
|
|
|
240
387
|
});
|
|
241
388
|
}
|
|
242
389
|
|
|
390
|
+
|
|
243
391
|
export default function (pi: ExtensionAPI) {
|
|
244
392
|
registerProfileCommand(pi, "openpi", "Manage openpi native profiles");
|
|
245
393
|
registerProfileCommand(pi, "azpi", "Deprecated alias for /openpi");
|
|
@@ -31,6 +31,7 @@ type TreeNode = {
|
|
|
31
31
|
name: string;
|
|
32
32
|
relativePath: string;
|
|
33
33
|
isDirectory: boolean;
|
|
34
|
+
sizeBytes?: number;
|
|
34
35
|
children?: TreeNode[];
|
|
35
36
|
};
|
|
36
37
|
|
|
@@ -39,6 +40,7 @@ type SearchQuery = {
|
|
|
39
40
|
cwd?: string;
|
|
40
41
|
globs?: string[];
|
|
41
42
|
caseInsensitive?: boolean;
|
|
43
|
+
isRegex?: boolean;
|
|
42
44
|
before?: number;
|
|
43
45
|
after?: number;
|
|
44
46
|
maxResults?: number;
|
|
@@ -83,6 +85,7 @@ function buildTree(params: {
|
|
|
83
85
|
maxDepth: number;
|
|
84
86
|
maxEntries: number;
|
|
85
87
|
includeHidden: boolean;
|
|
88
|
+
showSizes: boolean;
|
|
86
89
|
}): { nodes: TreeNode[]; omitted: number; visited: number } {
|
|
87
90
|
const ignoreNames = readIgnoreNames(params.projectRoot);
|
|
88
91
|
let visited = 0;
|
|
@@ -120,7 +123,11 @@ function buildTree(params: {
|
|
|
120
123
|
children: walk(absolutePath, depth + 1),
|
|
121
124
|
});
|
|
122
125
|
} else {
|
|
123
|
-
|
|
126
|
+
let sizeBytes: number | undefined;
|
|
127
|
+
if (params.showSizes) {
|
|
128
|
+
try { sizeBytes = fs.statSync(absolutePath).size; } catch { /* skip */ }
|
|
129
|
+
}
|
|
130
|
+
nodes.push({ name: entry.name, relativePath, isDirectory: false, sizeBytes });
|
|
124
131
|
}
|
|
125
132
|
}
|
|
126
133
|
return nodes;
|
|
@@ -129,12 +136,19 @@ function buildTree(params: {
|
|
|
129
136
|
return { nodes: walk(params.root, 0), omitted, visited };
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
function formatSize(bytes: number): string {
|
|
140
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
141
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
142
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
143
|
+
}
|
|
144
|
+
|
|
132
145
|
function renderTree(nodes: TreeNode[], prefix = ""): string[] {
|
|
133
146
|
const lines: string[] = [];
|
|
134
147
|
nodes.forEach((node, index) => {
|
|
135
148
|
const isLast = index === nodes.length - 1;
|
|
136
149
|
const connector = isLast ? "`-- " : "|-- ";
|
|
137
|
-
|
|
150
|
+
const sizeSuffix = node.sizeBytes !== undefined ? ` (${formatSize(node.sizeBytes)})` : "";
|
|
151
|
+
lines.push(`${prefix}${connector}${node.name}${node.isDirectory ? "/" : ""}${sizeSuffix}`);
|
|
138
152
|
if (node.children?.length) {
|
|
139
153
|
lines.push(...renderTree(node.children, `${prefix}${isLast ? " " : "| "}`));
|
|
140
154
|
}
|
|
@@ -145,6 +159,7 @@ function renderTree(nodes: TreeNode[], prefix = ""): string[] {
|
|
|
145
159
|
function runRipgrep(projectRoot: string, query: SearchQuery): Promise<string> {
|
|
146
160
|
const cwd = resolveProjectPath(projectRoot, query.cwd);
|
|
147
161
|
const args = ["--line-number", "--column", "--no-heading", "--color", "never"];
|
|
162
|
+
if (!query.isRegex) args.push("-F");
|
|
148
163
|
if (query.caseInsensitive) args.push("-i");
|
|
149
164
|
if (query.before) args.push("-B", String(query.before));
|
|
150
165
|
if (query.after) args.push("-A", String(query.after));
|
|
@@ -193,13 +208,15 @@ export default function searchToolsExtension(pi: ExtensionAPI) {
|
|
|
193
208
|
maxDepth: Type.Optional(Type.Number({ description: "Maximum directory depth. Default 3." })),
|
|
194
209
|
maxEntries: Type.Optional(Type.Number({ description: "Maximum entries to include. Default 400." })),
|
|
195
210
|
includeHidden: Type.Optional(Type.Boolean({ description: "Include hidden files and directories. Default false." })),
|
|
211
|
+
showSizes: Type.Optional(Type.Boolean({ description: "Show file sizes next to filenames. Default false." })),
|
|
196
212
|
}),
|
|
197
213
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
198
214
|
const root = resolveProjectPath(ctx.cwd, params.root as string | undefined);
|
|
199
215
|
const maxDepth = Math.max(0, Math.min(Number(params.maxDepth ?? 3), 8));
|
|
200
216
|
const maxEntries = Math.max(20, Math.min(Number(params.maxEntries ?? 400), 4000));
|
|
201
217
|
const includeHidden = Boolean(params.includeHidden);
|
|
202
|
-
const
|
|
218
|
+
const showSizes = Boolean(params.showSizes);
|
|
219
|
+
const result = buildTree({ projectRoot: ctx.cwd, root, maxDepth, maxEntries, includeHidden, showSizes });
|
|
203
220
|
const header = [
|
|
204
221
|
`root: ${normalizeRelative(ctx.cwd, root)}`,
|
|
205
222
|
`depth: ${maxDepth}`,
|
|
@@ -236,6 +253,7 @@ export default function searchToolsExtension(pi: ExtensionAPI) {
|
|
|
236
253
|
cwd: Type.Optional(Type.String({ description: "Project-relative directory to search in." })),
|
|
237
254
|
globs: Type.Optional(Type.Array(Type.String(), { description: "Optional ripgrep -g patterns." })),
|
|
238
255
|
caseInsensitive: Type.Optional(Type.Boolean({ description: "Use case-insensitive search." })),
|
|
256
|
+
isRegex: Type.Optional(Type.Boolean({ description: "Treat pattern as regex. Default false (literal/fixed-string search)." })),
|
|
239
257
|
before: Type.Optional(Type.Number({ description: "Context lines before each match." })),
|
|
240
258
|
after: Type.Optional(Type.Number({ description: "Context lines after each match." })),
|
|
241
259
|
maxResults: Type.Optional(Type.Number({ description: "Maximum matches per file from rg -m." })),
|
package/extensions/workflow.ts
CHANGED
|
@@ -20,6 +20,7 @@ import { StringEnum } from "@earendil-works/pi-ai";
|
|
|
20
20
|
import { type ExtensionAPI, getAgentDir, parseFrontmatter, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { Text } from "@earendil-works/pi-tui";
|
|
22
22
|
import { Type } from "typebox";
|
|
23
|
+
import { writeAuditLog } from "./lib/auditLogger.ts";
|
|
23
24
|
|
|
24
25
|
type AgentScope = "user" | "project" | "both";
|
|
25
26
|
type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
|
|
@@ -28,6 +29,8 @@ type ExecutionMode = "parallel" | "sequential";
|
|
|
28
29
|
const MAX_AGENTS_PER_CALL = 8;
|
|
29
30
|
const MAX_PARALLEL_CONCURRENCY = 4;
|
|
30
31
|
const OUTPUT_PREVIEW_CHARS = 6_000;
|
|
32
|
+
const DEFAULT_AGENT_TIMEOUT_MS = 120_000;
|
|
33
|
+
const RETRY_DELAY_MS = 2_000;
|
|
31
34
|
const ROLE_ALIASES: Record<string, string> = {
|
|
32
35
|
"file_picker": "file-picker",
|
|
33
36
|
"file-picker": "file-picker",
|
|
@@ -83,6 +86,8 @@ interface AgentRunRequest {
|
|
|
83
86
|
model?: string;
|
|
84
87
|
thinking?: ThinkingLevel;
|
|
85
88
|
tools?: string[];
|
|
89
|
+
timeout?: number;
|
|
90
|
+
retry?: boolean;
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
interface UsageStats {
|
|
@@ -108,6 +113,7 @@ interface AgentRunResult {
|
|
|
108
113
|
thinking?: ThinkingLevel;
|
|
109
114
|
stopReason?: string;
|
|
110
115
|
errorMessage?: string;
|
|
116
|
+
retryCount?: number;
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
interface SpawnAgentsDetails {
|
|
@@ -271,6 +277,7 @@ async function runSingleAgent(
|
|
|
271
277
|
availableAgents: AgentConfig[],
|
|
272
278
|
request: AgentRunRequest,
|
|
273
279
|
signal: AbortSignal | undefined,
|
|
280
|
+
onProgress?: (agentName: string, text: string) => void,
|
|
274
281
|
): Promise<AgentRunResult> {
|
|
275
282
|
const requestedAgent = normalizeRoleName(request.agent ?? request.agent_type);
|
|
276
283
|
const prompt = (request.prompt ?? request.task ?? "").trim();
|
|
@@ -330,6 +337,7 @@ async function runSingleAgent(
|
|
|
330
337
|
stdio: ["ignore", "pipe", "pipe"],
|
|
331
338
|
});
|
|
332
339
|
let buffer = "";
|
|
340
|
+
let progressText = "";
|
|
333
341
|
|
|
334
342
|
const processLine = (line: string) => {
|
|
335
343
|
if (!line.trim()) return;
|
|
@@ -340,6 +348,13 @@ async function runSingleAgent(
|
|
|
340
348
|
return;
|
|
341
349
|
}
|
|
342
350
|
|
|
351
|
+
// Stream text deltas for live progress
|
|
352
|
+
if (event.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
|
|
353
|
+
const delta = event.assistantMessageEvent.delta || "";
|
|
354
|
+
progressText += delta;
|
|
355
|
+
if (onProgress) onProgress(requestedAgent, progressText);
|
|
356
|
+
}
|
|
357
|
+
|
|
343
358
|
if (event.type === "message_end" && event.message) {
|
|
344
359
|
const message = event.message as Message;
|
|
345
360
|
messages.push(message);
|
|
@@ -397,17 +412,42 @@ async function runSingleAgent(
|
|
|
397
412
|
if (signal.aborted) killProc();
|
|
398
413
|
else signal.addEventListener("abort", killProc, { once: true });
|
|
399
414
|
}
|
|
415
|
+
|
|
416
|
+
// Agent timeout
|
|
417
|
+
const timeoutMs = request.timeout ?? DEFAULT_AGENT_TIMEOUT_MS;
|
|
418
|
+
if (timeoutMs > 0) {
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
if (!proc.killed) {
|
|
421
|
+
wasAborted = true;
|
|
422
|
+
baseResult.stopReason = "timeout";
|
|
423
|
+
proc.kill("SIGTERM");
|
|
424
|
+
setTimeout(() => {
|
|
425
|
+
if (!proc.killed) proc.kill("SIGKILL");
|
|
426
|
+
}, 5_000);
|
|
427
|
+
}
|
|
428
|
+
}, timeoutMs);
|
|
429
|
+
}
|
|
400
430
|
});
|
|
401
431
|
|
|
402
432
|
if (wasAborted) {
|
|
403
|
-
return { ...baseResult, exitCode, errorMessage: "Subagent was aborted" };
|
|
433
|
+
return { ...baseResult, exitCode, errorMessage: baseResult.stopReason === "timeout" ? `Agent timed out after ${(request.timeout ?? DEFAULT_AGENT_TIMEOUT_MS) / 1000}s` : "Subagent was aborted" };
|
|
404
434
|
}
|
|
405
435
|
|
|
406
|
-
|
|
436
|
+
const result: AgentRunResult = {
|
|
407
437
|
...baseResult,
|
|
408
438
|
exitCode,
|
|
409
439
|
output: getFinalAssistantText(messages),
|
|
410
440
|
};
|
|
441
|
+
|
|
442
|
+
// Retry logic: retry once if failed with empty output (transient failure)
|
|
443
|
+
if (request.retry && exitCode !== 0 && !result.output.trim() && !(result.retryCount ?? 0)) {
|
|
444
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
445
|
+
const retryResult = await runSingleAgent(projectCwd, availableAgents, { ...request, retry: false }, signal, onProgress);
|
|
446
|
+
retryResult.retryCount = 1;
|
|
447
|
+
return retryResult;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return result;
|
|
411
451
|
} finally {
|
|
412
452
|
if (tmpPromptPath) await fs.promises.rm(tmpPromptPath, { force: true }).catch(() => undefined);
|
|
413
453
|
if (tmpPromptDir) await fs.promises.rm(tmpPromptDir, { recursive: true, force: true }).catch(() => undefined);
|
|
@@ -439,12 +479,13 @@ function summarizeSpawnResults(results: AgentRunResult[]): string {
|
|
|
439
479
|
.map((result, index) => {
|
|
440
480
|
const ok = result.exitCode === 0 && !result.errorMessage;
|
|
441
481
|
const usage = formatUsage(result.usage);
|
|
482
|
+
const retryNote = result.retryCount ? ` (retried ${result.retryCount}x)` : "";
|
|
442
483
|
const meta = [result.model, result.thinking ? `thinking:${result.thinking}` : undefined, usage]
|
|
443
484
|
.filter(Boolean)
|
|
444
485
|
.join(" | ");
|
|
445
486
|
const body = result.output || result.errorMessage || result.stderr || "(no output)";
|
|
446
487
|
return [
|
|
447
|
-
`## ${index + 1}. ${ok ? "OK" : "FAIL"} ${result.agent}`,
|
|
488
|
+
`## ${index + 1}. ${ok ? "OK" : "FAIL"} ${result.agent}${retryNote}`,
|
|
448
489
|
meta ? `_${meta}_` : undefined,
|
|
449
490
|
"",
|
|
450
491
|
truncateText(body),
|
|
@@ -478,6 +519,8 @@ const AgentRequestSchema = Type.Object({
|
|
|
478
519
|
}),
|
|
479
520
|
),
|
|
480
521
|
tools: Type.Optional(Type.Array(Type.String(), { description: "Optional tool override for this agent process" })),
|
|
522
|
+
timeout: Type.Optional(Type.Number({ description: "Timeout in milliseconds for this agent. Default 120000 (2 minutes)." })),
|
|
523
|
+
retry: Type.Optional(Type.Boolean({ description: "Retry once on transient failure (empty output + non-zero exit). Default false." })),
|
|
481
524
|
});
|
|
482
525
|
|
|
483
526
|
export default function workflowExtension(pi: ExtensionAPI) {
|
|
@@ -543,7 +586,14 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
543
586
|
|
|
544
587
|
if (mode === "sequential") {
|
|
545
588
|
for (const request of requests) {
|
|
546
|
-
const
|
|
589
|
+
const runStart = Date.now();
|
|
590
|
+
const result = await runSingleAgent(ctx.cwd, discovery.agents, request, signal, (agentName, text) => {
|
|
591
|
+
onUpdate?.({
|
|
592
|
+
content: [{ type: "text", text: `spawn_agents [${agentName}]: ${text.split("\n").filter(Boolean).pop() || "working..."}` }],
|
|
593
|
+
details: makeDetails([...results]),
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
(result as any).elapsedMs = Date.now() - runStart;
|
|
547
597
|
results.push(result);
|
|
548
598
|
emitProgress();
|
|
549
599
|
if (result.exitCode !== 0 || result.errorMessage) break;
|
|
@@ -553,7 +603,14 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
553
603
|
requests,
|
|
554
604
|
MAX_PARALLEL_CONCURRENCY,
|
|
555
605
|
async (request) => {
|
|
556
|
-
const
|
|
606
|
+
const runStart = Date.now();
|
|
607
|
+
const result = await runSingleAgent(ctx.cwd, discovery.agents, request, signal, (agentName, text) => {
|
|
608
|
+
onUpdate?.({
|
|
609
|
+
content: [{ type: "text", text: `spawn_agents [${agentName}]: ${text.split("\n").filter(Boolean).pop() || "working..."}` }],
|
|
610
|
+
details: makeDetails([...results]),
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
(result as any).elapsedMs = Date.now() - runStart;
|
|
557
614
|
results.push(result);
|
|
558
615
|
emitProgress();
|
|
559
616
|
return result;
|
|
@@ -563,6 +620,21 @@ export default function workflowExtension(pi: ExtensionAPI) {
|
|
|
563
620
|
}
|
|
564
621
|
|
|
565
622
|
const successCount = results.filter((result) => result.exitCode === 0 && !result.errorMessage).length;
|
|
623
|
+
|
|
624
|
+
for (const res of results) {
|
|
625
|
+
writeAuditLog(ctx.cwd, {
|
|
626
|
+
type: "workflow",
|
|
627
|
+
name: `spawn_agents:${res.agent}`,
|
|
628
|
+
task: res.prompt,
|
|
629
|
+
input: res.prompt,
|
|
630
|
+
output: res.output || res.errorMessage || "",
|
|
631
|
+
exitCode: res.exitCode,
|
|
632
|
+
elapsedMs: (res as any).elapsedMs || 0,
|
|
633
|
+
|
|
634
|
+
metadata: { executionMode: mode, success: res.exitCode === 0 && !res.errorMessage }
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
|
|
566
638
|
return {
|
|
567
639
|
content: [
|
|
568
640
|
{
|
package/package.json
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matyah00/openpi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Pi-native
|
|
5
|
+
"description": "Comprehensive, high-performance, and safety-hardened Pi-native multi-agent orchestration package, featuring advanced workflows, damage-control rules, and developer tooling.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-coding-agent",
|
|
9
9
|
"commands",
|
|
10
10
|
"skills",
|
|
11
11
|
"agents",
|
|
12
|
-
"workflows"
|
|
12
|
+
"workflows",
|
|
13
|
+
"orchestration",
|
|
14
|
+
"safety",
|
|
15
|
+
"multi-agent",
|
|
16
|
+
"damage-control"
|
|
13
17
|
],
|
|
14
18
|
"homepage": "https://github.com/haytamAroui/OpenPi#readme",
|
|
15
19
|
"repository": {
|
package/prompts/docs.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate or update documentation with gap detection and review
|
|
3
|
+
category: quality
|
|
4
|
+
aliases:
|
|
5
|
+
- document
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Generate or update documentation for:
|
|
9
|
+
|
|
10
|
+
$ARGUMENTS
|
|
11
|
+
|
|
12
|
+
Process:
|
|
13
|
+
|
|
14
|
+
1. Use `project_tree` to map the project structure.
|
|
15
|
+
2. Use `code_search_batch` to find exports, public APIs, route definitions, and type declarations.
|
|
16
|
+
3. Identify documentation gaps:
|
|
17
|
+
- Missing README sections (setup, usage, API, contributing)
|
|
18
|
+
- Undocumented public functions, classes, or endpoints
|
|
19
|
+
- Stale documentation that doesn't match current code
|
|
20
|
+
- Missing architecture or design decision records
|
|
21
|
+
4. Use `spawn_agents` with `docs-writer` for detailed documentation generation when useful.
|
|
22
|
+
5. Use `spawn_agents` with `reviewer` to validate accuracy of generated docs.
|
|
23
|
+
|
|
24
|
+
Rules:
|
|
25
|
+
|
|
26
|
+
- Match existing documentation style and conventions.
|
|
27
|
+
- Include working code examples.
|
|
28
|
+
- Document error cases and edge cases.
|
|
29
|
+
- Use concrete language, not abstract descriptions.
|
|
30
|
+
- Link between related sections.
|
|
31
|
+
- Include prerequisites and setup steps.
|
|
32
|
+
|
|
33
|
+
Output:
|
|
34
|
+
|
|
35
|
+
- Generated or updated documentation files.
|
|
36
|
+
- List of remaining documentation gaps.
|
|
37
|
+
- Review verdict on documentation accuracy.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Plan and execute migrations with risk assessment, phased execution, and rollback strategies
|
|
3
|
+
category: planning
|
|
4
|
+
aliases:
|
|
5
|
+
- upgrade
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Plan a migration for:
|
|
9
|
+
|
|
10
|
+
$ARGUMENTS
|
|
11
|
+
|
|
12
|
+
Process:
|
|
13
|
+
|
|
14
|
+
1. Use `env_scan` to identify the current stack and versions.
|
|
15
|
+
2. Use `dependency_inventory` to audit current dependencies.
|
|
16
|
+
3. Use `code_search_batch` to find all touchpoints for the migration target.
|
|
17
|
+
4. Use `spawn_agents` with `migration-expert` for detailed migration analysis.
|
|
18
|
+
5. If the migration is risky (data loss possible, breaking changes), use `spawn_agents` with `red-team` to challenge the plan.
|
|
19
|
+
|
|
20
|
+
Output:
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
Migration: {from} → {to}
|
|
24
|
+
|
|
25
|
+
Risk: low | medium | high - {why}
|
|
26
|
+
|
|
27
|
+
Current state:
|
|
28
|
+
- ...
|
|
29
|
+
|
|
30
|
+
Breaking changes:
|
|
31
|
+
1. ...
|
|
32
|
+
|
|
33
|
+
Phases:
|
|
34
|
+
1. {phase} - rollback: {strategy}
|
|
35
|
+
2. ...
|
|
36
|
+
|
|
37
|
+
Validation:
|
|
38
|
+
- {command or check}
|
|
39
|
+
|
|
40
|
+
Point of no return:
|
|
41
|
+
- {phase N} — after this, rollback requires {strategy}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Do not execute destructive operations. Present the plan and wait for approval.
|